Compare commits

..

387 Commits

Author SHA1 Message Date
David Baker d764abc1d1 v0.9.2-cryptowraning.1 2018-03-26 14:11:29 +01:00
David Baker ee0f8abe71 Prepare changelog for v0.9.2-cryptowraning.1 2018-03-26 14:11:29 +01:00
David Baker b45e231968 Merge pull request #631 from matrix-org/dbkr/cryptowarning
Disable crypto if indexeddb version too new
2018-03-26 14:04:42 +01:00
David Baker c37db5ab9e Remove added whitespace 2018-03-26 13:57:37 +01:00
David Baker 77abc45489 Disable crypto if indexeddb version too new 2018-03-23 17:29:35 +00:00
Luke Barnard 678ff23bcb v0.9.2 2017-12-04 11:57:05 +00:00
Luke Barnard f93d50dcd0 Prepare changelog for v0.9.2 2017-12-04 11:57:04 +00:00
Luke Barnard aa3201ebb0 Send correct m.visibility flag in js-sdk 2017-12-04 11:54:42 +00:00
David Baker 5b1a5b7dd0 v0.9.1 2017-11-17 15:55:21 +00:00
David Baker f25324fb1c Prepare changelog for v0.9.1 2017-11-17 15:55:20 +00:00
David Baker 2a7f35a633 Merge pull request #577 from matrix-org/dbkr/fix_force_turn
Fix the force TURN option
2017-11-17 15:51:01 +00:00
David Baker eb2d5484b8 jsdoc and remove pointless return 2017-11-17 15:31:50 +00:00
David Baker 40cbd5ec9d Honour old forceTURN option for now 2017-11-17 15:03:12 +00:00
David Baker 10680ace17 Fix the force TURN option
Option needed to be passed in when creating a webrtc call, but for
incoming calls the js-sdk creates the call itself, so the app never
gets a chance to set the option.
2017-11-17 14:51:37 +00:00
Luke Barnard 3091a76702 v0.9.0 2017-11-15 10:56:31 +00:00
Luke Barnard 117e2370d7 Prepare changelog for v0.9.0 2017-11-15 10:56:31 +00:00
Luke Barnard a60e4efe6d v0.9.0-rc.1 2017-11-10 13:30:24 +00:00
Luke Barnard 4c0ebeab58 Prepare changelog for v0.9.0-rc.1 2017-11-10 13:30:24 +00:00
David Baker 0143ac2a86 Ignore pushrules in our cached sync response (#571)
* Ignore pushrules in our cached sync response

As hopefully explained in the comment
2017-11-10 13:14:54 +00:00
Luke Barnard 497a2bd057 Update API wrapper for group room association configuration (#569)
This is now a generic API on the homeserver. This is wrapped in a non-generic js-sdk function for now. In future, other group server implementations may want specific configuration and the configuration keys are namespaced to prevent collisions there.
2017-11-09 17:34:26 +00:00
Travis Ralston bf462e2840 Install babel-runtime, as it is required for operation (#561)
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-11-08 14:34:08 +00:00
Matthew Hodgson fb75134179 unbreak tests 2017-11-06 15:17:06 +00:00
Matthew Hodgson 68d67b7fc2 Remove ugly english non-i18n invite descriptions
any sensible app should be synthesising a better name for invites which is i18n-aware.
2017-11-06 11:18:24 +00:00
David Baker bbf412f3ad Merge pull request #567 from matrix-org/luke/groups-update-group-room-assoc
Modify addRoomToGroup to allow setting isPublic, create alias updateGroupRoomAssociation
2017-11-02 14:55:01 +00:00
David Baker 52b575296c Merge pull request #565 from matrix-org/dbkr/get_rule_by_id
Expose more functionality of pushprocessor
2017-11-02 13:49:00 +00:00
David Baker c1652f4898 Merge pull request #566 from matrix-org/dbkr/may_trigger_notif
Function for working out notif trigger permission
2017-11-02 13:48:07 +00:00
Luke Barnard d40cc795f7 Modify addRoomToGroup to allow setting isPublic, create alias updateGroupRoomAssociation
The API on synapse was modified to be an upsert, which means the same API call for adding a room to a group can be used to update the visibility of that association.
2017-11-02 13:34:34 +00:00
David Baker 4035d933ad Use the default power level if there's no PL event 2017-11-02 13:28:58 +00:00
David Baker d255348762 Remove leave check
1. It's wrong because it doesn't consider the ban state
2. This should be caught by whether they have permission to send
   the event in the first place rather than worrying about whether
   it has notification permission or not
2017-11-02 13:26:39 +00:00
David Baker 978db89deb Function for working out notif trigger permission
And make pushprocessor use this function
2017-11-02 12:04:55 +00:00
David Baker b867afe772 lint 2017-11-01 19:48:12 +00:00
David Baker e34fd89bb6 Expose more functionality of pushprocessor
This exposes a function to get rules by their ID and
ruleMatchesEvent to test whether a rule matches a given event.
2017-11-01 19:40:50 +00:00
Richard van der Hoff 51883b8f11 Merge pull request #555 from uhoreg/e2e_replay
keep track of event ID and timestamp of decrypted messages
2017-10-25 14:14:33 +01:00
David Baker ff5f95227a Merge pull request #564 from matrix-org/dbkr/fix_notifevents
Fix notifEvent computation
2017-10-25 12:06:05 +01:00
David Baker dc929236a4 oops, events not event 2017-10-25 12:03:17 +01:00
David Baker b09e20747a Oops, missing this 2017-10-25 12:00:11 +01:00
David Baker e8fc857dbc Don't calculate notifEvents until ready
We were previously computing notifEvents at the point where we
processed them but before the room they belong to was stored.
This was problematic because some push conditions try to get the
room and therefore failed. Since the push actions are cached, this
spurious calculation also got cached.

This moves the calculation out to a separate function that gets
called only after the room has been stored.
2017-10-25 11:54:44 +01:00
David Baker b42a13cc3b Merge pull request #563 from matrix-org/dbkr/fix_sentinels_on_power_levels
Fix power level of sentinel members
2017-10-25 11:47:18 +01:00
David Baker 37ed9800c5 Fix sender_notification_permission cond
Don't assume all events have a sender, because they don't.
2017-10-25 11:10:55 +01:00
David Baker 99f4968888 Replace all the sentinel members on power_levels
Rather than trying to be clever and keep ones whose power level isn't
affected, since they'll still have an associated power_levels event
that isn't the one actually in effect, which could be confusing.
2017-10-25 10:57:28 +01:00
David Baker 34428a811c Fix setPowerLevelEvent() semantics
* Make setPowerLevelEvent() use getDirectionalContent() rather than
   getContent(), otherwise power level events from pagination set
   power levels the wrong way
 * Don't assume the presence of a `users` key since the first
   power_levels event in a room will have no prev_content
2017-10-25 10:40:15 +01:00
Richard van der Hoff da7104b00d Merge pull request #554 from uhoreg/e2e_redact
don't try to decrypt a redacted message (fixes vector-im/riot-web#3744)
2017-10-24 21:02:01 +01:00
Hubert Chathi b373601e13 make sure redacted e2e messages are returned as a promise
Signed-off-by: Hubert Chathi <hubert@uhoreg.ca>
2017-10-24 13:11:48 -04:00
David Baker fcc90e884b Fix power level of sentinel members
When a ppower_levels event arrived, the current member objects were
being updated but the sentinel members were not, meaning that the
power levels of RoomMember objects associated with events had stale
power levels. This was causing power level based push rules to
be wrong.
2017-10-23 19:07:04 +01:00
David Baker 7a90096077 Merge pull request #562 from matrix-org/dbkr/room_notifs
Support room notifs
2017-10-23 17:01:23 +01:00
David Baker e7886a55fe lint 2017-10-23 16:50:01 +01:00
David Baker e33b786f65 Support room notifs
Specifically, add support for the sender_notification_permission
push rule condition. Also delays calculating notification events
until after they're added to the timeline, as per comment.
2017-10-23 16:02:06 +01:00
Luke Barnard b0b50f4ef9 Implement API wrapper for GET /group/$groupId/invited_users (#560) 2017-10-16 18:23:40 +02:00
David Baker 37c8e6dc07 Merge branch 'master' into develop 2017-10-16 14:37:39 +01:00
David Baker 675441fe93 v0.8.5 2017-10-16 14:35:20 +01:00
David Baker 2495d97862 Prepare changelog for v0.8.5 2017-10-16 14:35:20 +01:00
David Baker 70c998dd55 Merge pull request #558 from matrix-org/dbkr/fix_glob_to_regex
Fix the glob-to-regex code
2017-10-16 14:04:14 +01:00
David Baker ec60e46611 Merge remote-tracking branch 'origin/develop' into release-v0.8.5 2017-10-16 14:03:04 +01:00
David Baker bb2dc618f7 Merge pull request #559 from matrix-org/dbkr/unknown_condition_dont_match
Make unknown pushrule conditions not match
2017-10-16 14:02:02 +01:00
David Baker d9addf84ef Make unknown pushrule conditions not match
Unknown conditions previously were assumed to match, but given rules
can be added on the server as base rules, it's probably better to not
match unknown conditions.
2017-10-16 13:55:07 +01:00
David Baker d89163110e Fix the glob-to-regex code
It was only making one replacement so would fail for anything with
more than ine glob special char (eg. *foo would be fine, but *foo*
was broken)
2017-10-16 13:40:49 +01:00
David Baker 93b722af3c v0.8.5-rc.1 2017-10-13 11:01:58 +01:00
David Baker ee0e036aa5 Prepare changelog for v0.8.5-rc.1 2017-10-13 11:01:57 +01:00
David Baker 24ef004adc remove debug logging 2017-10-12 16:03:08 +01:00
Hubert Chathi dcab4eb70b fix lint error, and incorporate suggestions from richvdh and krombel
Signed-off-by: Hubert Chathi <hubert@uhoreg.ca>
2017-10-12 09:04:12 -04:00
Hubert Chathi 8f252992e4 keep track of event ID and timestamp of decrypted messages
This is to avoid false positives when detecting replay attacks.

fixes: vector-im/riot-web#3712

Signed-off-by: Hubert Chathi <hubert@uhoreg.ca>
2017-10-11 23:27:36 -04:00
Hubert Chathi a3a3e32e21 don't try to decrypt a redacted message (fixes vector-im/riot-web#3744)
set the decrypted data to an empty message

Signed-off-by: Hubert Chathi <hubert@uhoreg.ca>
2017-10-04 12:26:08 -04:00
David Baker f5f8867326 Merge pull request #553 from matrix-org/luke/groups-remove-room
Implement wrapper API for removing a room from a group
2017-10-04 10:58:28 +01:00
Luke Barnard 5494a9dd3b Implement wrapper API for removing a room from a group 2017-09-29 17:57:53 +01:00
David Baker 89f7857c84 Merge pull request #552 from matrix-org/rav/stuck_key_download
Fix typo which resulted in stuck key download requests
2017-09-29 10:22:26 +01:00
Richard van der Hoff 3140c6526a Fix typo which resulted in stuck key download requests
(... and hence stuck outgoing e2e messages)

Fixes https://github.com/vector-im/riot-web/issues/4278
2017-09-29 10:02:16 +01:00
Luke Barnard 96d828d22b Implement API wrapper for adding a room to a group (#551) 2017-09-27 14:27:19 +01:00
David Baker a8a50384b8 Add wrapper for group update_publicity API (#550)
Add wrapper for group update_publicity API
2017-09-25 15:27:59 +01:00
David Baker 787af6448d Merge pull request #549 from matrix-org/dbkr/groups_store_when_created
Store group when it's created
2017-09-22 18:02:17 +01:00
David Baker 4633135322 Store group when it's created
Otherwise we emit events when we set properties on the group but
getGroup() for that group returns null which is very confusing.
2017-09-22 17:57:08 +01:00
David Baker 050d522735 Merge pull request #548 from matrix-org/luke/groups-remove-rooms-users-from-summary
Luke/groups remove rooms users from summary
2017-09-22 16:23:47 +01:00
Luke Barnard 809b202e70 Merge branch 'develop' into luke/groups-remove-rooms-users-from-summary 2017-09-22 16:19:17 +01:00
Luke Barnard f1555dbb5d Implement wrapper APIs for removing users and rooms from group summary 2017-09-22 16:18:18 +01:00
krombel d1d0266a10 reduce sendToDevice payload (#522)
instead of sending one huge request split them up.
2017-09-22 14:29:23 +01:00
David Baker ee37ed0697 Add a clean target (#547) 2017-09-21 21:45:55 +01:00
David Baker e3972dee2c Merge branch 'master' into develop 2017-09-21 21:41:48 +01:00
David Baker 7a727e7eda v0.8.4 2017-09-21 21:39:37 +01:00
David Baker d24c822f68 Prepare changelog for v0.8.4 2017-09-21 21:39:37 +01:00
Richard van der Hoff 8d804013f3 Merge pull request #546 from matrix-org/dbkr/clean_on_prerelease
Clean on prepublish
2017-09-21 18:06:28 +01:00
David Baker d73a115436 Clean on prerelease
Otherwise you can make broken releases on case insensitive file
systems
2017-09-21 18:02:30 +01:00
Luke Barnard 0b4ff731e8 Implement wrapper APIs for adding rooms to group summary (#545) 2017-09-21 17:05:36 +01:00
Luke Barnard a58ec3f192 Implement wrapper APIs for adding rooms to group summary 2017-09-21 17:01:33 +01:00
Richard van der Hoff 868c20b161 Fix a race in decrypting megolm messages (#544)
* Fix a race in decrypting megolm messages

This fixes a race wherein it was possible for us to fail to decrypt a message,
if the keys arrived immediately after our attempt to decrypt it. In that case,
a retry *should* have been scheduled, but was not.

Fixes https://github.com/vector-im/riot-web/issues/5001.

* WORDS
2017-09-21 15:17:28 +01:00
Luke Barnard c2cd050419 Implement API to add users to group summaries (#543)
(and call it addUserToGroupSummary)
2017-09-20 16:31:28 +01:00
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
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
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
69 changed files with 6689 additions and 2472 deletions
+11 -1
View File
@@ -1,3 +1,13 @@
{
"presets": ["es2015"]
"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",
],
}
+4
View File
@@ -9,6 +9,10 @@ module.exports = {
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: {
+3 -1
View File
@@ -1,4 +1,6 @@
.jsdoc
/.jsdocbuild
/.jsdoc
node_modules
.lock-wscript
build/Release
+1 -2
View File
@@ -2,5 +2,4 @@ language: node_js
node_js:
- node # Latest stable version of nodejs.
script:
- npm run lint
- npm run test
- ./travis.sh
+332
View File
@@ -1,3 +1,335 @@
Changes in [0.9.2-cryptowraning.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.9.2-cryptowraning.1) (2018-03-26)
================================================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.9.2...v0.9.2-cryptowraning.1)
* Disable crypto if indexeddb version too new
[\#631](https://github.com/matrix-org/matrix-js-sdk/pull/631)
Changes in [0.9.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.9.2) (2017-12-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.9.1...v0.9.2)
Changes in [0.9.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.9.1) (2017-11-17)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.9.0...v0.9.1)
* Fix the force TURN option
[\#577](https://github.com/matrix-org/matrix-js-sdk/pull/577)
Changes in [0.9.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.9.0) (2017-11-15)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.9.0-rc.1...v0.9.0)
Changes in [0.9.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.9.0-rc.1) (2017-11-10)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.5...v0.9.0-rc.1)
* Modify addRoomToGroup to allow setting isPublic, create alias
updateGroupRoomAssociation
[\#567](https://github.com/matrix-org/matrix-js-sdk/pull/567)
* Expose more functionality of pushprocessor
[\#565](https://github.com/matrix-org/matrix-js-sdk/pull/565)
* Function for working out notif trigger permission
[\#566](https://github.com/matrix-org/matrix-js-sdk/pull/566)
* keep track of event ID and timestamp of decrypted messages
[\#555](https://github.com/matrix-org/matrix-js-sdk/pull/555)
* Fix notifEvent computation
[\#564](https://github.com/matrix-org/matrix-js-sdk/pull/564)
* Fix power level of sentinel members
[\#563](https://github.com/matrix-org/matrix-js-sdk/pull/563)
* don't try to decrypt a redacted message (fixes vector-im/riot-web#3744)
[\#554](https://github.com/matrix-org/matrix-js-sdk/pull/554)
* Support room notifs
[\#562](https://github.com/matrix-org/matrix-js-sdk/pull/562)
* Fix the glob-to-regex code
[\#558](https://github.com/matrix-org/matrix-js-sdk/pull/558)
Changes in [0.8.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.5) (2017-10-16)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.5-rc.1...v0.8.5)
* Make unknown pushrule conditions not match
[\#559](https://github.com/matrix-org/matrix-js-sdk/pull/559)
Changes in [0.8.5-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.5-rc.1) (2017-10-13)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.4...v0.8.5-rc.1)
* Implement wrapper API for removing a room from a group
[\#553](https://github.com/matrix-org/matrix-js-sdk/pull/553)
* Fix typo which resulted in stuck key download requests
[\#552](https://github.com/matrix-org/matrix-js-sdk/pull/552)
* Store group when it's created
[\#549](https://github.com/matrix-org/matrix-js-sdk/pull/549)
* Luke/groups remove rooms users from summary
[\#548](https://github.com/matrix-org/matrix-js-sdk/pull/548)
* Clean on prepublish
[\#546](https://github.com/matrix-org/matrix-js-sdk/pull/546)
* Implement wrapper APIs for adding rooms to group summary
[\#545](https://github.com/matrix-org/matrix-js-sdk/pull/545)
Changes in [0.8.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.4) (2017-09-21)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.3...v0.8.4)
* Fix build issue
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)
+44 -2
View File
@@ -13,7 +13,8 @@ In a browser
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.
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.
@@ -29,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?
----------------------
@@ -236,6 +238,46 @@ host the API reference from the source files like this:
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
+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;
+15 -15
View File
@@ -1,20 +1,22 @@
{
"name": "matrix-js-sdk",
"version": "0.7.8",
"version": "0.9.2-cryptowraning.1",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"test:build": "babel -s -d specbuild spec",
"test:run": "istanbul cover --report text --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" _mocha -- --recursive specbuild --colors --reporter mocha-jenkins-reporter --reporter-options junit_report_path=reports/test-results.xml",
"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": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
"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",
"clean": "rimraf lib dist",
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && 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 115 src spec",
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt"
"lint": "eslint --max-warnings 109 src spec",
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt"
},
"repository": {
"url": "https://github.com/matrix-org/matrix-js-sdk"
@@ -48,13 +50,17 @@
],
"dependencies": {
"another-json": "^0.2.0",
"babel-runtime": "^6.26.0",
"bluebird": "^3.5.0",
"browser-request": "^0.3.3",
"q": "^1.4.1",
"content-type": "^1.0.2",
"request": "^2.53.0"
},
"devDependencies": {
"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",
@@ -63,8 +69,9 @@
"exorcist": "^0.4.0",
"expect": "^1.20.2",
"istanbul": "^0.4.5",
"jsdoc": "^3.4.0",
"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",
@@ -73,16 +80,9 @@
"uglify-js": "^2.8.26",
"watchify": "^3.2.1"
},
"optionalDependencies": {
"olm": "https://matrix.org/packages/npm/olm/olm-2.2.1.tgz"
},
"browserify": {
"transform": [
"sourceify",
"browserify-shim"
"sourceify"
]
},
"browserify-shim": {
"olm": "global:Olm"
}
}
+3 -1
View File
@@ -206,7 +206,9 @@ if [ -n "$signing_id" ]; then
tarfile="$tag.tar.gz"
gh_project_url=$(git remote get-url origin |
sed -e 's#^git@github.com:#https://github.com/#' -e 's/\.git$//')
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}"
+37 -7
View File
@@ -17,11 +17,14 @@ 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 './mock-request';
import MockHttpBackend from 'matrix-mock-request';
import expect from 'expect';
import q from 'q';
import Promise from 'bluebird';
/**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
@@ -30,12 +33,20 @@ import q from 'q';
* @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) {
export default function TestClient(
userId, deviceId, accessToken, sessionStoreBackend,
) {
this.userId = userId;
this.deviceId = deviceId;
this.storage = new sdk.WebStorageSessionStore(new testUtils.MockStorageApi());
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",
@@ -74,7 +85,10 @@ TestClient.prototype.start = function() {
pendingEventOrdering: 'detached',
});
return this.httpBackend.flush().then(() => {
return Promise.all([
this.httpBackend.flushAllExpected(),
testUtils.syncPromise(this.client),
]).then(() => {
console.log(this + ': started');
});
};
@@ -115,7 +129,7 @@ TestClient.prototype.expectDeviceKeyUpload = function() {
TestClient.prototype.awaitOneTimeKeyUpload = function() {
if (Object.keys(this.oneTimeKeys).length != 0) {
// already got one-time keys
return q(this.oneTimeKeys);
return Promise.resolve(this.oneTimeKeys);
}
this.httpBackend.when("POST", "/keys/upload")
@@ -140,7 +154,8 @@ TestClient.prototype.awaitOneTimeKeyUpload = function() {
}};
});
return this.httpBackend.flush('/keys/upload', 2).then((flushed) => {
// this can take ages
return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => {
expect(flushed).toEqual(2);
return this.oneTimeKeys;
});
@@ -184,3 +199,18 @@ 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();
}
});
});
});
+158 -150
View File
@@ -25,9 +25,13 @@ limitations under the License.
"use strict";
import 'source-map-support/register';
// load olm before the sdk if possible
import '../olm-loader';
import expect from 'expect';
const sdk = require("../..");
const q = require("q");
import Promise from 'bluebird';
const utils = require("../../lib/utils");
const testUtils = require("../test-utils");
const TestClient = require('../TestClient').default;
@@ -46,7 +50,7 @@ let bobMessages;
function bobUploadsDeviceKeys() {
bobTestClient.expectDeviceKeyUpload();
return q.all([
return Promise.all([
bobTestClient.client.uploadKeys(),
bobTestClient.httpBackend.flush(),
]).then(() => {
@@ -129,8 +133,8 @@ function expectAliClaimKeys() {
// it can take a while to process the key query, so give it some extra
// time, and make sure the claim actually happens rather than ploughing on
// confusingly.
return aliTestClient.httpBackend.flush("/keys/claim", 1, 20).then((r) => {
expect(r).toEqual(1);
return aliTestClient.httpBackend.flush("/keys/claim", 1, 500).then((r) => {
expect(r).toEqual(1, "Ali did not claim Bob's keys");
});
});
}
@@ -141,19 +145,16 @@ function aliDownloadsKeys() {
expect(bobTestClient.getSigningKey()).toBeTruthy();
const p1 = aliTestClient.client.downloadKeys([bobUserId]).then(function() {
expect(aliTestClient.client.listDeviceKeys(bobUserId)).toEqual([{
id: "bvcxz",
key: bobTestClient.getSigningKey(),
verified: false,
blocked: false,
display_name: null,
}]);
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
}).then((devices) => {
expect(devices.length).toEqual(1);
expect(devices[0].deviceId).toEqual("bvcxz");
});
const p2 = expectAliQueryKeys();
// check that the localStorage is updated as we expect (not sure this is
// an integration test, but meh)
return q.all([p1, p2]).then(function() {
return Promise.all([p1, p2]).then(function() {
const devices = aliTestClient.storage.getEndToEndDevicesForUser(bobUserId);
expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys);
expect(devices[bobDeviceId].verified).
@@ -184,7 +185,7 @@ function bobEnablesEncryption() {
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function aliSendsFirstMessage() {
return q.all([
return Promise.all([
sendMessage(aliTestClient.client),
expectAliQueryKeys()
.then(expectAliClaimKeys)
@@ -201,7 +202,7 @@ function aliSendsFirstMessage() {
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function aliSendsMessage() {
return q.all([
return Promise.all([
sendMessage(aliTestClient.client),
expectAliSendMessageRequest(),
]).spread(function(_, ciphertext) {
@@ -216,7 +217,7 @@ function aliSendsMessage() {
* @return {promise} which resolves to the ciphertext for Ali's device.
*/
function bobSendsReplyMessage() {
return q.all([
return Promise.all([
sendMessage(bobTestClient.client),
expectBobQueryKeys()
.then(expectBobSendMessageRequest),
@@ -265,7 +266,7 @@ function sendMessage(client) {
function expectSendMessageRequest(httpBackend) {
const path = "/send/m.room.encrypted/";
const deferred = q.defer();
const deferred = Promise.defer();
httpBackend.when("PUT", path).respond(200, function(path, content) {
deferred.resolve(content);
return {
@@ -273,8 +274,8 @@ function expectSendMessageRequest(httpBackend) {
};
});
// it can take a while to process the key query, so give it 20ms
return httpBackend.flush(path, 1, 20).then(() => deferred.promise);
// it can take a while to process the key query
return httpBackend.flush(path, 1).then(() => deferred.promise);
}
function aliRecvMessage() {
@@ -313,31 +314,37 @@ function recvMessage(httpBackend, client, sender, message) {
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
const deferred = q.defer();
const onEvent = function(event) {
console.log(client.credentials.userId + " received event",
event);
// ignore the m.room.member events
if (event.getType() == "m.room.member") {
return;
}
const eventPromise = new Promise((resolve, reject) => {
const onEvent = function(event) {
// ignore the m.room.member events
if (event.getType() == "m.room.member") {
return;
}
console.log(client.credentials.userId + " received event",
event);
client.removeListener("event", onEvent);
resolve(event);
};
client.on("event", onEvent);
});
httpBackend.flush();
return eventPromise.then((event) => {
expect(event.isEncrypted()).toBeTruthy();
// it may still be being decrypted
return testUtils.awaitDecryption(event);
}).then((event) => {
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toEqual({
msgtype: "m.text",
body: "Hello, World",
});
expect(event.isEncrypted()).toBeTruthy();
client.removeListener("event", onEvent);
deferred.resolve();
};
client.on("event", onEvent);
httpBackend.flush();
return deferred.promise;
});
}
@@ -375,9 +382,7 @@ function firstSync(testClient) {
};
testClient.httpBackend.when("GET", "/sync").respond(200, syncData);
return testClient.httpBackend.flush("/sync", 1).then(() => {
return testUtils.syncPromise(testClient.client);
});
return testClient.flushSync();
}
@@ -386,11 +391,14 @@ describe("MatrixClient crypto", function() {
return;
}
beforeEach(function() {
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
await aliTestClient.client.initCrypto();
bobTestClient = new TestClient(bobUserId, bobDeviceId, bobAccessToken);
await bobTestClient.client.initCrypto();
aliMessages = [];
bobMessages = [];
@@ -404,19 +412,19 @@ describe("MatrixClient crypto", function() {
});
it("Bob uploads device keys", function() {
return q()
return Promise.resolve()
.then(bobUploadsDeviceKeys);
});
it("Ali downloads Bobs device keys", function(done) {
q()
Promise.resolve()
.then(bobUploadsDeviceKeys)
.then(aliDownloadsKeys)
.catch(testUtils.failTest).done(done);
.nodeify(done);
});
it("Ali gets keys with an invalid signature", function(done) {
q()
Promise.resolve()
.then(bobUploadsDeviceKeys)
.then(function() {
// tamper bob's keys
@@ -424,14 +432,17 @@ describe("MatrixClient crypto", function() {
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy();
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
return q.all(aliTestClient.client.downloadKeys([bobUserId]),
expectAliQueryKeys());
})
.then(function() {
return Promise.all([
aliTestClient.client.downloadKeys([bobUserId]),
expectAliQueryKeys(),
]);
}).then(function() {
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
}).then((devices) => {
// should get an empty list
expect(aliTestClient.client.listDeviceKeys(bobUserId)).toEqual([]);
expect(devices).toEqual([]);
})
.catch(testUtils.failTest).done(done);
.nodeify(done);
});
it("Ali gets keys with an incorrect userId", function(done) {
@@ -463,14 +474,19 @@ describe("MatrixClient crypto", function() {
return {device_keys: result};
});
q.all(
Promise.all([
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
aliTestClient.httpBackend.flush("/keys/query", 1),
).then(function() {
]).then(function() {
return Promise.all([
aliTestClient.client.getStoredDevicesForUser(bobUserId),
aliTestClient.client.getStoredDevicesForUser(eveUserId),
]);
}).spread((bobDevices, eveDevices) => {
// should get an empty list
expect(aliTestClient.client.listDeviceKeys(bobUserId)).toEqual([]);
expect(aliTestClient.client.listDeviceKeys(eveUserId)).toEqual([]);
}).catch(testUtils.failTest).done(done);
expect(bobDevices).toEqual([]);
expect(eveDevices).toEqual([]);
}).nodeify(done);
});
it("Ali gets keys with an incorrect deviceId", function(done) {
@@ -500,18 +516,20 @@ describe("MatrixClient crypto", function() {
return {device_keys: result};
});
q.all(
Promise.all([
aliTestClient.client.downloadKeys([bobUserId]),
aliTestClient.httpBackend.flush("/keys/query", 1),
).then(function() {
]).then(function() {
return aliTestClient.client.getStoredDevicesForUser(bobUserId);
}).then((devices) => {
// should get an empty list
expect(aliTestClient.client.listDeviceKeys(bobUserId)).toEqual([]);
}).catch(testUtils.failTest).done(done);
expect(devices).toEqual([]);
}).nodeify(done);
});
it("Bob starts his client and uploads device keys and one-time keys", function() {
return q()
return Promise.resolve()
.then(() => bobTestClient.start())
.then(() => bobTestClient.awaitOneTimeKeyUpload())
.then((keys) => {
@@ -521,28 +539,27 @@ describe("MatrixClient crypto", function() {
});
it("Ali sends a message", function(done) {
q()
Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.catch(testUtils.failTest).nodeify(done);
.nodeify(done);
});
it("Bob receives a message", function(done) {
q()
it("Bob receives a message", function() {
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobRecvMessage)
.catch(testUtils.failTest).done(done);
.then(bobRecvMessage);
});
it("Bob receives a message with a bogus sender", function(done) {
q()
it("Bob receives a message with a bogus sender", function() {
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
@@ -572,34 +589,30 @@ describe("MatrixClient crypto", function() {
};
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
const deferred = q.defer();
const onEvent = function(event) {
console.log(bobUserId + " received event",
event);
// ignore the m.room.member events
if (event.getType() == "m.room.member") {
return;
}
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
expect(event.isEncrypted()).toBeTruthy();
bobTestClient.client.removeListener("event", onEvent);
deferred.resolve();
};
bobTestClient.client.on("event", onEvent);
const eventPromise = new Promise((resolve, reject) => {
const onEvent = function(event) {
console.log(bobUserId + " received event",
event);
resolve(event);
};
bobTestClient.client.once("event", onEvent);
});
bobTestClient.httpBackend.flush();
return deferred.promise;
})
.catch(testUtils.failTest).done(done);
return eventPromise;
}).then((event) => {
expect(event.isEncrypted()).toBeTruthy();
// it may still be being decrypted
return testUtils.awaitDecryption(event);
}).then((event) => {
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
});
});
it("Ali blocks Bob's device", function(done) {
q()
Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
@@ -613,12 +626,12 @@ describe("MatrixClient crypto", function() {
// no unblocked devices, so the ciphertext should be empty
expect(sentContent.ciphertext).toEqual({});
});
return q.all([p1, p2]);
return Promise.all([p1, p2]);
}).nodeify(done);
});
it("Bob receives two pre-key messages", function(done) {
q()
Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
@@ -627,11 +640,11 @@ describe("MatrixClient crypto", function() {
.then(bobRecvMessage)
.then(aliSendsMessage)
.then(bobRecvMessage)
.catch(testUtils.failTest).done(done);
.nodeify(done);
});
it("Bob replies to the message", function() {
return q()
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
.then(() => firstSync(aliTestClient))
@@ -645,64 +658,10 @@ describe("MatrixClient crypto", function() {
}).then(aliRecvMessage);
});
it("Ali does a key query when she gets a new_device event", function() {
return q()
.then(() => aliTestClient.start())
.then(() => firstSync(aliTestClient))
// ali will only care about bob's new_device if she is tracking
// bob's devices, which she will do if we enable encryption
.then(aliEnablesEncryption)
.then(() => {
aliTestClient.expectKeyQuery({
device_keys: {
[aliUserId]: {},
[bobUserId]: {},
},
});
return aliTestClient.httpBackend.flush('/keys/query', 1);
})
// make sure that the initial key download has completed
// (downloadKeys will wait until it does)
.then(() => {
return aliTestClient.client.downloadKeys([bobUserId]);
})
.then(function() {
const syncData = {
next_batch: '2',
to_device: {
events: [
testUtils.mkEvent({
content: {
device_id: 'TEST_DEVICE',
rooms: [],
},
sender: bobUserId,
type: 'm.new_device',
}),
],
},
};
aliTestClient.httpBackend.when('GET', '/sync').respond(200, syncData);
return aliTestClient.httpBackend.flush('/sync', 1);
}).then(() => {
aliTestClient.expectKeyQuery({
device_keys: {
[bobUserId]: {},
},
});
return aliTestClient.httpBackend.flush('/keys/query', 1);
});
});
it("Ali does a key query when encryption is enabled", function() {
// enabling encryption in the room should make alice download devices
// for both members.
return q()
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => firstSync(aliTestClient))
.then(() => {
@@ -736,7 +695,56 @@ describe("MatrixClient crypto", function() {
[bobUserId]: {},
},
});
return aliTestClient.httpBackend.flush('/keys/query', 1);
return aliTestClient.httpBackend.flushAllExpected();
});
});
it("Upload new oneTimeKeys based on a /sync request - no count-asking", function() {
// Send a response which causes a key upload
const httpBackend = aliTestClient.httpBackend;
const syncDataEmpty = {
next_batch: "a",
device_one_time_keys_count: {
signed_curve25519: 0,
},
};
// enqueue expectations:
// * Sync with empty one_time_keys => upload keys
return Promise.resolve()
.then(() => {
console.log(aliTestClient + ': starting');
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
aliTestClient.expectDeviceKeyUpload();
// we let the client do a very basic initial sync, which it needs before
// it will upload one-time keys.
httpBackend.when("GET", "/sync").respond(200, syncDataEmpty);
aliTestClient.client.startClient({});
return httpBackend.flushAllExpected().then(() => {
console.log(aliTestClient + ': started');
});
})
.then(() => httpBackend.when("POST", "/keys/upload")
.respond(200, (path, content) => {
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).toNotEqual({});
expect(Object.keys(content.one_time_keys).length)
.toBeGreaterThanOrEqualTo(1);
console.log('received %i one-time keys',
Object.keys(content.one_time_keys).length);
// cancel futher calls by telling the client
// we have more than we need
return {
one_time_key_counts: {
signed_curve25519: 70,
},
};
}))
.then(() => httpBackend.flushAllExpected());
});
});
+28 -17
View File
@@ -1,10 +1,11 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
import expect from 'expect';
import Promise from 'bluebird';
describe("MatrixClient events", function() {
const baseUrl = "http://localhost.or.something";
@@ -101,7 +102,7 @@ describe("MatrixClient events", function() {
};
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);
@@ -130,11 +131,16 @@ describe("MatrixClient events", function() {
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",
);
done();
});
});
@@ -157,13 +163,13 @@ describe("MatrixClient events", function() {
});
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);
let roomInvokeCount = 0;
@@ -183,7 +189,10 @@ 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.",
);
@@ -193,11 +202,10 @@ describe("MatrixClient events", function() {
expect(timelineFireCount).toEqual(
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);
@@ -232,7 +240,10 @@ 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",
);
@@ -242,11 +253,10 @@ describe("MatrixClient events", function() {
expect(eventsInvokeCount).toEqual(
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);
@@ -271,7 +281,10 @@ 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",
);
@@ -284,11 +297,10 @@ describe("MatrixClient events", function() {
expect(membershipInvokeCount).toEqual(
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' });
let sessionLoggedOutCount = 0;
@@ -298,11 +310,10 @@ describe("MatrixClient events", function() {
client.startClient();
httpBackend.flush().done(function() {
return httpBackend.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(
1, "Session.logged_out fired wrong number of times",
);
done();
});
});
});
+191 -172
View File
@@ -1,8 +1,8 @@
"use strict";
import 'source-map-support/register';
const q = require("q");
import Promise from 'bluebird';
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
const EventTimeline = sdk.EventTimeline;
@@ -82,7 +82,7 @@ function startClient(httpBackend, client) {
client.startClient();
// set up a promise which will resolve once the client is initialised
const deferred = q.defer();
const deferred = Promise.defer();
client.on("sync", function(state) {
console.log("sync", state);
if (state != "SYNCING") {
@@ -91,8 +91,10 @@ function startClient(httpBackend, client) {
deferred.resolve();
});
httpBackend.flush();
return deferred.promise;
return Promise.all([
httpBackend.flushAllExpected(),
deferred.promise,
]);
}
describe("getEventTimeline support", function() {
@@ -125,10 +127,10 @@ describe("getEventTimeline support", function() {
expect(function() {
client.getEventTimeline(timelineSet, "event");
}).toThrow();
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
it("timeline support works when enabled", function(done) {
it("timeline support works when enabled", function() {
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
@@ -136,16 +138,13 @@ describe("getEventTimeline support", function() {
timelineSupport: true,
});
startClient(httpBackend, client,
).then(function() {
return startClient(httpBackend, client).then(() => {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
expect(function() {
client.getEventTimeline(timelineSet, "event");
}).toNotThrow();
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
});
@@ -196,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() {
@@ -215,7 +215,7 @@ 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);
});
});
@@ -225,7 +225,7 @@ describe("MatrixClient event timelines", function() {
let client = null;
let httpBackend = null;
beforeEach(function(done) {
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
@@ -237,8 +237,7 @@ describe("MatrixClient event timelines", function() {
timelineSupport: true,
});
startClient(httpBackend, client)
.catch(utils.failTest).done(done);
return startClient(httpBackend, client);
});
afterEach(function() {
@@ -247,7 +246,7 @@ describe("MatrixClient event timelines", function() {
});
describe("getEventTimeline", function() {
it("should create a new timeline for new events", function(done) {
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")
@@ -265,22 +264,23 @@ describe("MatrixClient event timelines", function() {
};
});
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");
}).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) {
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, {
@@ -299,17 +299,20 @@ describe("MatrixClient event timelines", function() {
},
});
httpBackend.flush("/sync").then(function() {
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() {
@@ -344,7 +347,7 @@ describe("MatrixClient event timelines", function() {
};
});
const deferred = q.defer();
const deferred = Promise.defer();
client.on("sync", function() {
client.getEventTimeline(timelineSet, EVENTS[2].event_id,
).then(function(tl) {
@@ -360,14 +363,14 @@ describe("MatrixClient event timelines", function() {
(e) => deferred.reject(e));
});
return q.all([
httpBackend.flush(),
return Promise.all([
httpBackend.flushAllExpected(),
deferred.promise,
]);
});
it("should join timelines where they overlap a previous /context",
function(done) {
function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
@@ -427,41 +430,42 @@ describe("MatrixClient event timelines", function() {
let tl0;
let tl3;
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl0 = tl;
return client.getEventTimeline(timelineSet, EVENTS[2].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl3 = tl;
return client.getEventTimeline(timelineSet, EVENTS[1].event_id);
}).then(function(tl) {
// we expect it to get merged in with event 2
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
expect(tl.getNeighbouringTimeline(EventTimeline.BACKWARDS))
.toBe(tl0);
expect(tl.getNeighbouringTimeline(EventTimeline.FORWARDS))
.toBe(tl3);
expect(tl0.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl0.getPaginationToken(EventTimeline.FORWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.BACKWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token3");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
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) {
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
@@ -477,20 +481,21 @@ describe("MatrixClient event timelines", function() {
};
});
client.getEventTimeline(timelineSet, "event1",
).then(function(tl) {
// could do with a fail()
expect(true).toBeFalsy();
}).catch(function(e) {
expect(String(e)).toMatch(/'event'/);
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
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) {
it("should allow you to paginate backwards", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
@@ -521,27 +526,28 @@ describe("MatrixClient event timelines", function() {
});
let tl;
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(tl, {backwards: true});
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[2]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[0]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token1");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token0");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
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) {
it("should allow you to paginate forwards", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
@@ -572,24 +578,25 @@ describe("MatrixClient event timelines", function() {
});
let tl;
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(
tl, {backwards: false, limit: 20});
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[0]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[2]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token1");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
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(),
]);
});
});
@@ -624,60 +631,66 @@ describe("MatrixClient event timelines", function() {
});
});
it("should work when /send returns before /sync", function(done) {
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(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");
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 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);
}).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) {
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(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (2)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
}).catch(utils.failTest).done(done);
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 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);
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);
}),
]);
});
});
@@ -714,7 +727,10 @@ describe("MatrixClient event timelines", function() {
};
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.flush().then(function() {
Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client),
]).then(function() {
const room = client.getRoom(roomId);
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(3);
@@ -739,11 +755,14 @@ describe("MatrixClient event timelines", function() {
};
httpBackend.when("GET", "/sync").respond(200, sync2);
return httpBackend.flush();
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);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
});
+25 -15
View File
@@ -1,7 +1,7 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const HttpBackend = require("matrix-mock-request");
const publicGlobals = require("../../lib/matrix");
const Room = publicGlobals.Room;
const MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
@@ -49,13 +49,16 @@ describe("MatrixClient", function() {
httpBackend.when(
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.data).toEqual(buf);
expect(req.rawData).toEqual(buf);
expect(req.queryParams.filename).toEqual("hi.txt");
expect(req.queryParams.access_token).toEqual(accessToken);
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");
}).respond(200, "content", true);
const prom = client.uploadContent({
stream: buf,
@@ -76,7 +79,7 @@ describe("MatrixClient", function() {
const uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(0);
}).catch(utils.failTest).done(done);
}).nodeify(done);
httpBackend.flush();
});
@@ -86,7 +89,7 @@ describe("MatrixClient", function() {
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.opts.json).toBeFalsy();
}).respond(200, JSON.stringify({ "content_uri": "uri" }));
}).respond(200, { "content_uri": "uri" });
client.uploadContent({
stream: buf,
@@ -96,22 +99,21 @@ describe("MatrixClient", function() {
rawResponse: false,
}).then(function(response) {
expect(response.content_uri).toEqual("uri");
}).catch(utils.failTest).done(done);
}).nodeify(done);
httpBackend.flush();
});
it("should parse errors into a MatrixError", function(done) {
// opts.json is false, so request returns unparsed json.
httpBackend.when(
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.data).toEqual(buf);
expect(req.rawData).toEqual(buf);
expect(req.opts.json).toBeFalsy();
}).respond(400, JSON.stringify({
}).respond(400, {
"errcode": "M_SNAFU",
"error": "broken",
}));
});
client.uploadContent({
stream: buf,
@@ -123,7 +125,7 @@ describe("MatrixClient", function() {
expect(error.httpStatus).toEqual(400);
expect(error.errcode).toEqual("M_SNAFU");
expect(error.message).toEqual("broken");
}).catch(utils.failTest).done(done);
}).nodeify(done);
httpBackend.flush();
});
@@ -147,7 +149,7 @@ describe("MatrixClient", function() {
const uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(0);
}).catch(utils.failTest).done(done);
}).nodeify(done);
const r = client.cancelUpload(prom);
expect(r).toBe(true);
@@ -299,6 +301,14 @@ 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) {
const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
// ed25519key = client.getDeviceEd25519Key();
@@ -375,7 +385,7 @@ describe("MatrixClient", function() {
algorithms: ["2"],
unsigned: { "ghi": "def" },
});
}).catch(utils.failTest).done(done);
}).nodeify(done);
httpBackend.flush();
});
@@ -392,7 +402,7 @@ describe("MatrixClient", function() {
client.deleteDevice(
"my_device", auth,
).catch(utils.failTest).done(done);
).nodeify(done);
httpBackend.flush();
});
+6 -4
View File
@@ -2,10 +2,11 @@
import 'source-map-support/register';
const sdk = require("../..");
const MatrixClient = sdk.MatrixClient;
const HttpBackend = require("../mock-request");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
import expect from 'expect';
import Promise from 'bluebird';
describe("MatrixClient opts", function() {
const baseUrl = "http://localhost.or.something";
@@ -113,9 +114,10 @@ describe("MatrixClient opts", function() {
httpBackend.flush("/pushrules", 1).then(function() {
return httpBackend.flush("/filter", 1);
}).then(function() {
return httpBackend.flush("/sync", 1);
}).then(function() {
return utils.syncPromise(client);
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]);
}).done(function() {
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes,
+29 -9
View File
@@ -1,7 +1,9 @@
"use strict";
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
const EventStatus = sdk.EventStatus;
@@ -52,15 +54,20 @@ 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
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
const tl = room.getLiveTimeline().getEvents();
expect(tl.length).toEqual(2);
@@ -86,7 +93,14 @@ describe("MatrixClient retrying", function() {
}).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);
@@ -94,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() {
+85 -75
View File
@@ -2,9 +2,10 @@
import 'source-map-support/register';
const sdk = require("../..");
const EventStatus = sdk.EventStatus;
const HttpBackend = require("../mock-request");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
import Promise from 'bluebird';
import expect from 'expect';
describe("MatrixClient room timelines", function() {
@@ -251,11 +252,14 @@ describe("MatrixClient room timelines", function() {
client.scrollback(room).done(function() {
expect(room.timeline.length).toEqual(1);
expect(room.oldState.paginationToken).toBe(null);
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);
});
@@ -317,11 +321,14 @@ describe("MatrixClient room timelines", function() {
expect(oldMsg.sender.name).toEqual("Old Alice");
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);
});
@@ -348,11 +355,14 @@ describe("MatrixClient room timelines", 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);
});
@@ -376,9 +386,11 @@ describe("MatrixClient room timelines", 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);
@@ -386,17 +398,17 @@ describe("MatrixClient room timelines", function() {
});
describe("new events", function() {
it("should be added to the right place in the timeline", function(done) {
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}),
];
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
let index = 0;
@@ -408,9 +420,10 @@ describe("MatrixClient room timelines", function() {
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).then(function() {
return utils.syncPromise(client);
}).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(
@@ -419,12 +432,11 @@ describe("MatrixClient room timelines", function() {
expect(room.timeline[1].event).toEqual(
eventData[0],
);
}).catch(utils.failTest).done(done);
});
});
httpBackend.flush("/sync", 1);
});
it("should set the right event.sender values", function(done) {
it("should set the right event.sender values", function() {
const eventData = [
utils.mkMessage({user: userId, room: roomId}),
utils.mkMembership({
@@ -435,24 +447,24 @@ describe("MatrixClient room timelines", function() {
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
httpBackend.flush("/sync", 1).then(function() {
return utils.syncPromise(client);
}).then(function() {
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) {
it("should set the right room.name", function() {
const secondRoomNameEvent = utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 2",
@@ -461,19 +473,20 @@ describe("MatrixClient room timelines", function() {
secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
setNextSyncData([secondRoomNameEvent]);
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
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).then(() => {
return utils.syncPromise(client);
}).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
@@ -485,19 +498,18 @@ describe("MatrixClient room timelines", function() {
thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
setNextSyncData([thirdRoomNameEvent]);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend.flush("/sync", 1).then(() => {
return utils.syncPromise(client);
}).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) {
it("should set the right room members", function() {
const userC = "@cee:bar";
const userD = "@dee:bar";
const eventData = [
@@ -512,14 +524,15 @@ describe("MatrixClient room timelines", function() {
eventData[1].__prev_event = null;
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
httpBackend.flush("/sync", 1).then(function() {
return utils.syncPromise(client);
}).then(function() {
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(
@@ -529,30 +542,30 @@ describe("MatrixClient room timelines", function() {
expect(room.currentState.getMember(userD).membership).toEqual(
"invite",
);
}).catch(utils.failTest).done(done);
});
});
httpBackend.flush("/sync", 1);
});
});
describe("gappy sync", function() {
it("should copy the last known state to the new timeline", function(done) {
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;
}
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).then(function() {
return utils.syncPromise(client);
}).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);
@@ -564,23 +577,21 @@ describe("MatrixClient room timelines", function() {
expect(room.currentState.getMember(otherUserId).membership).toEqual(
"join",
);
done();
});
});
httpBackend.flush("/sync", 1);
});
it("should emit a 'Room.timelineReset' event", function(done) {
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;
}
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(() => {
const room = client.getRoom(roomId);
let emitCount = 0;
@@ -590,14 +601,13 @@ describe("MatrixClient room timelines", function() {
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).then(function() {
return utils.syncPromise(client);
}).done(function() {
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]).then(function() {
expect(emitCount).toEqual(1);
done();
});
});
httpBackend.flush("/sync", 1);
});
});
});
+108 -62
View File
@@ -1,12 +1,13 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("../mock-request");
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() {
const baseUrl = "http://localhost.or.something";
@@ -51,7 +52,7 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flush().done(function() {
httpBackend.flushAllExpected().done(function() {
done();
});
});
@@ -65,7 +66,7 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flush().done(function() {
httpBackend.flushAllExpected().done(function() {
done();
});
});
@@ -113,7 +114,7 @@ describe("MatrixClient syncing", function() {
};
});
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,
@@ -132,17 +133,20 @@ describe("MatrixClient syncing", function() {
resolveInvitesToProfiles: true,
});
httpBackend.flush().done(function() {
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),
).toBeTruthy();
done();
});
});
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",
@@ -160,14 +164,16 @@ describe("MatrixClient syncing", function() {
resolveInvitesToProfiles: true,
});
httpBackend.flush().done(function() {
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",
@@ -192,13 +198,15 @@ describe("MatrixClient syncing", function() {
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,
@@ -209,13 +217,15 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flush().done(function() {
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),
).toBe(null);
done();
});
});
});
@@ -236,15 +246,17 @@ describe("MatrixClient syncing", function() {
};
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();
});
});
});
@@ -355,57 +367,66 @@ describe("MatrixClient syncing", function() {
},
};
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() {
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,
);
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() {
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() {
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() {
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]).then(function() {
const room = client.getRoom(roomTwo);
let member = room.getMember(otherUserId);
expect(member).toBeTruthy();
@@ -413,7 +434,6 @@ describe("MatrixClient syncing", function() {
member = room.getMember(selfUserId);
expect(member).toBeTruthy();
expect(member.typing).toEqual(false);
done();
});
});
@@ -448,10 +468,13 @@ 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) {
it("should set the back-pagination token on new rooms", function() {
const syncData = {
next_batch: "batch_token",
rooms: {
@@ -471,16 +494,19 @@ describe("MatrixClient syncing", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.flush().then(function() {
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) {
it("should set the back-pagination token on gappy syncs", function() {
const syncData = {
next_batch: "batch_token",
rooms: {
@@ -511,13 +537,15 @@ describe("MatrixClient syncing", function() {
expect(tok).toEqual("newerTok");
});
httpBackend.flush().then(function() {
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();
});
});
});
@@ -570,7 +598,7 @@ describe("MatrixClient syncing", function() {
};
});
it("should sync receipts from /sync.", function(done) {
it("should sync receipts from /sync.", function() {
const ackEvent = syncData.rooms.join[roomOne].timeline.events[0];
const receipt = {};
receipt[ackEvent.event_id] = {
@@ -588,7 +616,10 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flush().done(function() {
return Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]).then(function() {
const room = client.getRoom(roomOne);
expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{
type: "m.read",
@@ -597,7 +628,6 @@ describe("MatrixClient syncing", function() {
ts: 176592842636,
},
}]);
done();
});
});
});
@@ -617,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.
@@ -627,29 +657,34 @@ 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) {
it("should set the back-pagination token on left rooms", function() {
const syncData = {
next_batch: "batch_token",
rooms: {
@@ -674,20 +709,31 @@ describe("MatrixClient syncing", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
client.syncLeftRooms().then(function() {
const room = client.getRoom(roomTwo);
const 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);
}
});
@@ -16,18 +16,10 @@ limitations under the License.
"use strict";
import 'source-map-support/register';
let Olm = null;
try {
Olm = require('olm');
} catch (e) {}
const anotherjson = require('another-json');
const q = require('q');
import Promise from 'bluebird';
import expect from 'expect';
const sdk = require('../..');
const utils = require('../../lib/utils');
const testUtils = require('../test-utils');
const TestClient = require('../TestClient').default;
@@ -46,7 +38,7 @@ function createOlmSession(olmAccount, recipientTestClient) {
const otkId = utils.keys(keys)[0];
const otk = keys[otkId];
const session = new Olm.Session();
const session = new global.Olm.Session();
session.create_outbound(
olmAccount, recipientTestClient.getDeviceKey(), otk.key,
);
@@ -127,6 +119,7 @@ function encryptMegolmEvent(opts) {
}
return {
event_id: 'test_megolm_event',
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: opts.groupSession.encrypt(JSON.stringify(plaintext)),
@@ -209,9 +202,11 @@ function getSyncResponse(roomMembers) {
describe("megolm", function() {
if (!sdk.CRYPTO_ENABLED) {
if (!global.Olm) {
console.warn('not running megolm tests: Olm not present');
return;
}
const Olm = global.Olm;
let testOlmAccount;
let testSenderKey;
@@ -286,12 +281,13 @@ describe("megolm", function() {
return claimResponse;
}
beforeEach(function() {
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();
@@ -303,7 +299,7 @@ describe("megolm", function() {
aliceTestClient.stop();
});
it("Alice receives a megolm message", function(done) {
it("Alice receives a megolm message", function() {
return aliceTestClient.start().then(() => {
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
@@ -343,17 +339,18 @@ describe("megolm", function() {
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.httpBackend.flush("/sync", 1);
}).then(function() {
return testUtils.syncPromise(aliceTestClient.client);
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');
}).nodeify(done);
});
});
it("Alice receives a megolm message before the session keys", function(done) {
it("Alice receives a megolm message before the session keys", function() {
// https://github.com/vector-im/riot-web/issues/2273
let roomKeyEncrypted;
@@ -393,9 +390,7 @@ describe("megolm", function() {
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.httpBackend.flush("/sync", 1);
}).then(function() {
return testUtils.syncPromise(aliceTestClient.client);
return aliceTestClient.flushSync();
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
@@ -410,17 +405,27 @@ describe("megolm", function() {
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.httpBackend.flush("/sync", 1);
}).then(function() {
return testUtils.syncPromise(aliceTestClient.client);
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');
}).nodeify(done);
});
});
it("Alice gets a second room_key message", function(done) {
it("Alice gets a second room_key message", function() {
return aliceTestClient.start().then(() => {
return createOlmSession(testOlmAccount, aliceTestClient);
}).then((p2pSession) => {
@@ -480,17 +485,18 @@ describe("megolm", function() {
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse2);
return aliceTestClient.httpBackend.flush("/sync", 2);
}).then(function() {
return testUtils.syncPromise(aliceTestClient.client);
// 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');
}).nodeify(done);
});
});
it('Alice sends a megolm message', function(done) {
it('Alice sends a megolm message', function() {
let p2pSession;
return aliceTestClient.start().then(() => {
@@ -510,22 +516,20 @@ describe("megolm", function() {
syncResponse.to_device = { events: [olmEvent] };
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1);
}).then(function() {
return testUtils.syncPromise(aliceTestClient.client);
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 q.all([
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.flush(),
aliceTestClient.httpBackend.flushAllExpected(),
]);
}).then(function() {
// mark the device as known, and resend.
@@ -565,52 +569,18 @@ describe("megolm", function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const pendingMsg = room.getPendingEvents()[0];
return q.all([
return Promise.all([
aliceTestClient.client.resendEvent(pendingMsg, room),
aliceTestClient.httpBackend.flush(),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
}).nodeify(done);
});
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function(done) {
return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']);
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1);
}).then(function() {
return testUtils.syncPromise(aliceTestClient.client);
}).then(function() {
console.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:xyz': {},
},
});
return q.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
console.log("Telling alice to send a megolm message");
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, {
event_id: '$event_id',
});
return q.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
]);
}).nodeify(done);
});
it("We shouldn't attempt to send to blocked devices", function(done) {
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);
@@ -626,9 +596,7 @@ describe("megolm", function() {
syncResponse.to_device = { events: [olmEvent] };
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1);
}).then(function() {
return testUtils.syncPromise(aliceTestClient.client);
return aliceTestClient.flushSync();
}).then(function() {
console.log('Forcing alice to download our device keys');
@@ -636,7 +604,7 @@ describe("megolm", function() {
200, getTestKeysQueryResponse('@bob:xyz'),
);
return q.all([
return Promise.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
@@ -651,14 +619,18 @@ describe("megolm", function() {
event_id: '$event_id',
});
return q.all([
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
}).nodeify(done);
});
});
it("We should start a new megolm session when a device is blocked", function(done) {
it("We should start a new megolm session when a device is blocked", function() {
let p2pSession;
let megolmSessionId;
@@ -679,9 +651,7 @@ describe("megolm", function() {
syncResponse.to_device = { events: [olmEvent] };
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1);
}).then(function() {
return testUtils.syncPromise(aliceTestClient.client);
return aliceTestClient.flushSync();
}).then(function() {
console.log("Fetching bob's devices and marking known");
@@ -689,9 +659,9 @@ describe("megolm", function() {
200, getTestKeysQueryResponse('@bob:xyz'),
);
return q.all([
return Promise.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush(),
aliceTestClient.httpBackend.flushAllExpected(),
]).then((keys) => {
aliceTestClient.client.setDeviceKnown('@bob:xyz', 'DEVICE_ID');
});
@@ -722,9 +692,13 @@ describe("megolm", function() {
};
});
return q.all([
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
// 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');
@@ -741,15 +715,15 @@ describe("megolm", function() {
};
});
return q.all([
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),
aliceTestClient.httpBackend.flush(),
aliceTestClient.httpBackend.flushAllExpected(),
]);
}).nodeify(done);
});
});
// https://github.com/vector-im/riot-web/issues/2676
it("Alice should send to her other devices", function(done) {
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.
@@ -791,20 +765,17 @@ describe("megolm", function() {
getTestKeysQueryResponse(aliceTestClient.userId),
);
return aliceTestClient.httpBackend.flush();
return aliceTestClient.httpBackend.flushAllExpected();
}).then(function() {
// start out with the device unknown - the send should be rejected.
return q.all([
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']);
}),
aliceTestClient.httpBackend.flush(),
]);
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');
@@ -846,55 +817,26 @@ describe("megolm", function() {
};
});
return q.all([
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
// 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');
}).nodeify(done);
});
});
it('Alice should wait for device list to complete when sending a megolm message',
function(done) {
let p2pSession;
let inboundGroupSession;
function() {
let downloadPromise;
let sendPromise;
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',
};
});
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
@@ -910,13 +852,10 @@ describe("megolm", function() {
syncResponse.to_device = { events: [olmEvent] };
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1);
return aliceTestClient.flushSync();
}).then(function() {
return testUtils.syncPromise(aliceTestClient.client);
}).then(function() {
console.log('Forcing alice to download our device keys');
// this will block
console.log('Forcing alice to download our device keys');
downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
// so will this.
@@ -931,137 +870,14 @@ describe("megolm", function() {
200, getTestKeysQueryResponse('@bob:xyz'),
);
return aliceTestClient.httpBackend.flush();
return aliceTestClient.httpBackend.flushAllExpected();
}).then(function() {
return q.all([downloadPromise, sendPromise]);
}).nodeify(done);
return Promise.all([downloadPromise, sendPromise]);
});
});
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.httpBackend.flush('/sync', 1);
}).then(() => {
return testUtils.syncPromise(aliceTestClient.client);
}).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 q.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
() => aliceTestClient.httpBackend.flush('/send/', 1, 20),
),
]);
}).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'],
},
});
return aliceTestClient.httpBackend.flush('/sync', 2);
}).then(() => {
return testUtils.syncPromise(aliceTestClient.client);
}).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 ' +
bobStat);
}
// 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);
});
});
it("Alice exports megolm keys and imports them to a new device", function(done) {
it("Alice exports megolm keys and imports them to a new device", function() {
let messageEncrypted;
return aliceTestClient.start().then(() => {
@@ -1104,9 +920,7 @@ describe("megolm", function() {
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.httpBackend.flush("/sync", 1);
}).then(function() {
return testUtils.syncPromise(aliceTestClient.client);
return aliceTestClient.flushSync();
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
@@ -1120,10 +934,10 @@ describe("megolm", function() {
aliceTestClient = new TestClient(
"@alice:localhost", "device2", "access_token2",
);
aliceTestClient.client.importRoomKeys(exported);
return aliceTestClient.start();
return aliceTestClient.client.initCrypto().then(() => {
aliceTestClient.client.importRoomKeys(exported);
return aliceTestClient.start();
});
}).then(function() {
const syncResponse = {
next_batch: 1,
@@ -1138,13 +952,11 @@ describe("megolm", function() {
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.httpBackend.flush("/sync", 1);
}).then(function() {
return testUtils.syncPromise(aliceTestClient.client);
return aliceTestClient.flushSync();
}).then(function() {
const room = aliceTestClient.client.getRoom(ROOM_ID);
const event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42');
}).nodeify(done);
});
});
});
-295
View File
@@ -1,295 +0,0 @@
"use strict";
const q = require("q");
import expect from 'expect';
/**
* Construct a mock HTTP backend, heavily inspired by Angular.js.
* @constructor
*/
function HttpBackend() {
this.requests = [];
this.expectedRequests = [];
const self = this;
// the request function dependency that the SDK needs.
this.requestFn = function(opts, callback) {
const req = new Request(opts, callback);
console.log("HTTP backend received request: %s", req);
self.requests.push(req);
const abort = function() {
const idx = self.requests.indexOf(req);
if (idx >= 0) {
console.log("Aborting HTTP request: %s %s", opts.method,
opts.uri);
self.requests.splice(idx, 1);
req.callback("aborted");
}
};
return {
abort: abort,
};
};
}
HttpBackend.prototype = {
/**
* 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.
* @param {integer=} waitTime The time (in ms) to wait for a request to happen.
* default: 5
*
* @return {Promise} resolves when there is nothing left to flush, with the
* number of requests flushed
*/
flush: function(path, numToFlush, waitTime) {
const defer = q.defer();
const self = this;
let flushed = 0;
let triedWaiting = false;
if (waitTime === undefined) {
waitTime = 5;
}
console.log(
"HTTP backend flushing... (path=%s numToFlush=%s waitTime=%s)",
path, numToFlush, waitTime,
);
const tryFlush = function() {
// if there's more real requests and more expected requests, flush 'em.
console.log(
" trying to flush queue => reqs=[%s] expected=[%s]",
self.requests, self.expectedRequests.map((er) => er.path),
);
if (self._takeFromQueue(path)) {
// try again on the next tick.
flushed += 1;
if (numToFlush && flushed === numToFlush) {
console.log(" Flushed assigned amount: %s", numToFlush);
defer.resolve(flushed);
} else {
console.log(" flushed. Trying for more.");
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, waitTime);
triedWaiting = true;
} else {
console.log(" no more flushes.");
defer.resolve(flushed);
}
};
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) {
let req = null;
let i;
let j;
let matchingReq = null;
let expectedReq = null;
let 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);
let 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() {
const 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() {
const 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) {
const pendingReq = new ExpectedRequest(method, path, data);
this.expectedRequests.push(pendingReq);
return pendingReq;
},
};
/**
* Represents the expectation of a request.
*
* <p>Includes the conditions to be matched against, the checks to be made,
* and the response to be returned.
*
* @constructor
* @param {string} method
* @param {string} path
* @param {object?} data
*/
function ExpectedRequest(method, path, data) {
this.method = method;
this.path = path;
this.data = data;
this.response = null;
this.checks = [];
}
ExpectedRequest.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,
};
},
};
/**
* Represents a request made by the app.
*
* @constructor
* @param {object} opts opts passed to request()
* @param {function} callback
*/
function Request(opts, callback) {
this.opts = opts;
this.callback = callback;
Object.defineProperty(this, 'method', {
get: function() {
return opts.method;
},
});
Object.defineProperty(this, 'path', {
get: function() {
return opts.uri;
},
});
Object.defineProperty(this, 'data', {
get: function() {
return opts.body;
},
});
Object.defineProperty(this, 'queryParams', {
get: function() {
return opts.qs;
},
});
Object.defineProperty(this, 'headers', {
get: function() {
return opts.headers || {};
},
});
}
Request.prototype = {
toString: function() {
return this.method + " " + this.path;
},
};
/**
* The HttpBackend class.
*/
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");
}
+51 -42
View File
@@ -1,27 +1,43 @@
"use strict";
import expect from 'expect';
import q from 'q';
import Promise from 'bluebird';
const sdk = require("..");
// 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) {
const def = q.defer();
const cb = (state) => {
if (state == 'SYNCING') {
def.resolve();
} else {
client.once('sync', cb);
}
};
client.once('sync', cb);
return def.promise;
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);
});
};
/**
@@ -178,35 +194,6 @@ module.exports.mkMessage = function(opts) {
};
/**
* make the test fail, with the given exception
*
* <p>This is useful for use with integration tests which use asyncronous
* methods: it can be added as a 'catch' handler in a promise chain.
*
* @param {Error} err exception to be reported
*
* @deprecated
* It turns out there are easier ways of doing this. Just use nodeify():
*
* it("should not throw", function(done) {
* asynchronousMethod().then(function() {
* // some tests
* }).nodeify(done);
* });
*
* @example
* it("should not throw", function(done) {
* asynchronousMethod().then(function() {
* // some tests
* }).catch(utils.failTest).done(done);
* });
*/
module.exports.failTest = function(err) {
expect(true).toBe(false, "Testfunc threw: " + err.stack);
};
/**
* A mock implementation of webstorage
*
@@ -232,3 +219,25 @@ module.exports.MockStorageApi.prototype = {
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);
});
});
};
+5 -5
View File
@@ -5,7 +5,7 @@ import testUtils from '../../test-utils';
import utils from '../../../lib/utils';
import expect from 'expect';
import q from 'q';
import Promise from 'bluebird';
const signedDeviceList = {
"failures": {},
@@ -64,7 +64,7 @@ describe('DeviceList', function() {
dl.startTrackingDeviceList('@test1:sw1v.org');
const queryDefer1 = q.defer();
const queryDefer1 = Promise.defer();
downloadSpy.andReturn(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
@@ -83,7 +83,7 @@ describe('DeviceList', function() {
dl.startTrackingDeviceList('@test1:sw1v.org');
const queryDefer1 = q.defer();
const queryDefer1 = Promise.defer();
downloadSpy.andReturn(queryDefer1.promise);
const prom1 = dl.refreshOutdatedDeviceLists();
@@ -91,7 +91,7 @@ describe('DeviceList', function() {
downloadSpy.reset();
// outdated notif arrives while the request is in flight.
const queryDefer2 = q.defer();
const queryDefer2 = Promise.defer();
downloadSpy.andReturn(queryDefer2.promise);
dl.invalidateUserDeviceList('@test1:sw1v.org');
@@ -110,7 +110,7 @@ describe('DeviceList', function() {
console.log("Creating new devicelist to simulate app reload");
downloadSpy.reset();
const dl2 = createTestDeviceList();
const queryDefer3 = q.defer();
const queryDefer3 = Promise.defer();
downloadSpy.andReturn(queryDefer3.promise);
const prom3 = dl2.refreshOutdatedDeviceLists();
+268
View File
@@ -0,0 +1,268 @@
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();
});
});
it("can detect replay attacks", function() {
// trying to decrypt two different messages (marked by different
// event IDs or timestamps) using the same (sender key, session id,
// message index) triple should result in an exception being thrown
// as it should be detected as a replay attack.
const sessionId = groupSession.session_id();
const cipherText = groupSession.encrypt(JSON.stringify({
room_id: ROOM_ID,
content: 'testytest',
}));
const event1 = new MatrixEvent({
type: 'm.room.encrypted',
room_id: ROOM_ID,
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: "SENDER_CURVE25519",
session_id: sessionId,
ciphertext: cipherText,
},
event_id: "$event1",
origin_server_ts: 1507753886000,
});
const successHandler = expect.createSpy();
const failureHandler = expect.createSpy()
.andCall((err) => {
expect(err.toString()).toMatch(
/Duplicate message index, possible replay attack/,
);
});
return megolmDecryption.decryptEvent(event1).then((res) => {
const event2 = new MatrixEvent({
type: 'm.room.encrypted',
room_id: ROOM_ID,
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: "SENDER_CURVE25519",
session_id: sessionId,
ciphertext: cipherText,
},
event_id: "$event2",
origin_server_ts: 1507754149000,
});
return megolmDecryption.decryptEvent(event2);
}).then(
successHandler,
failureHandler,
).then(() => {
expect(successHandler).toNotHaveBeenCalled();
expect(failureHandler).toHaveBeenCalled();
});
});
it("allows re-decryption of the same event", function() {
// in contrast with the previous test, if the event ID and
// timestamp are the same, then it should not be considered a
// replay attack
const sessionId = groupSession.session_id();
const cipherText = groupSession.encrypt(JSON.stringify({
room_id: ROOM_ID,
content: 'testytest',
}));
const event = new MatrixEvent({
type: 'm.room.encrypted',
room_id: ROOM_ID,
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: "SENDER_CURVE25519",
session_id: sessionId,
ciphertext: cipherText,
},
event_id: "$event1",
origin_server_ts: 1507753886000,
});
return megolmDecryption.decryptEvent(event).then((res) => {
return megolmDecryption.decryptEvent(event);
// test is successful if no exception is thrown
});
});
});
});
+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;
});
});
});
});
+5 -5
View File
@@ -16,7 +16,7 @@ limitations under the License.
"use strict";
import 'source-map-support/register';
const q = require("q");
import Promise from 'bluebird';
const sdk = require("../..");
const utils = require("../test-utils");
@@ -81,14 +81,14 @@ describe("InteractiveAuth", function() {
type: "logintype",
foo: "bar",
});
return q(requestRes);
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);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
it("should make a request if no authdata is provided", function(done) {
@@ -138,7 +138,7 @@ describe("InteractiveAuth", function() {
type: "logintype",
foo: "bar",
});
return q(requestRes);
return Promise.resolve(requestRes);
});
ia.submitAuthDict({
@@ -151,6 +151,6 @@ describe("InteractiveAuth", function() {
expect(res).toBe(requestRes);
expect(doRequest.calls.length).toEqual(2);
expect(stateUpdated.calls.length).toEqual(1);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
});
+10 -10
View File
@@ -1,6 +1,6 @@
"use strict";
import 'source-map-support/register';
const q = require("q");
import Promise from 'bluebird';
const sdk = require("../..");
const MatrixClient = sdk.MatrixClient;
const utils = require("../test-utils");
@@ -62,7 +62,7 @@ describe("MatrixClient", function() {
let pendingLookup = null;
function httpReq(cb, method, path, qp, data, prefix) {
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
return q();
return Promise.resolve();
}
const next = httpLookups.shift();
const logLine = (
@@ -84,7 +84,7 @@ describe("MatrixClient", function() {
);
}
pendingLookup = {
promise: q.defer().promise,
promise: Promise.defer().promise,
method: method,
path: path,
};
@@ -109,7 +109,7 @@ describe("MatrixClient", function() {
}
if (next.error) {
return q.reject({
return Promise.reject({
errcode: next.error.errcode,
httpStatus: next.error.httpStatus,
name: next.error.errcode,
@@ -117,10 +117,10 @@ describe("MatrixClient", function() {
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() {
@@ -136,8 +136,8 @@ describe("MatrixClient", function() {
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter",
"getSyncAccumulator", "startup", "deleteAllData",
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
store.getSavedSync = expect.createSpy().andReturn(q(null));
store.setSyncData = expect.createSpy().andReturn(q(null));
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,
@@ -174,10 +174,10 @@ describe("MatrixClient", function() {
// a DIFFERENT test (pollution between tests!) - we return unresolved
// promises to stop the client from continuing to run.
client._http.authedRequest.andCall(function() {
return q.defer().promise;
return Promise.defer().promise;
});
client._http.authedRequestWithPrefix.andCall(function() {
return q.defer().promise;
return Promise.defer().promise;
});
});
+6 -6
View File
@@ -404,7 +404,7 @@ describe("Room", function() {
it("should copy state from previous timeline", function() {
room.addLiveEvents([events[0], events[1]]);
expect(room.getLiveTimeline().getEvents().length).toEqual(2);
room.resetLiveTimeline();
room.resetLiveTimeline('sometoken', 'someothertoken');
room.addLiveEvents([events[2]]);
const oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS);
@@ -417,7 +417,7 @@ describe("Room", function() {
it("should reset the legacy timeline fields", function() {
room.addLiveEvents([events[0], events[1]]);
expect(room.timeline.length).toEqual(2);
room.resetLiveTimeline();
room.resetLiveTimeline('sometoken', 'someothertoken');
room.addLiveEvents([events[2]]);
const newLiveTimeline = room.getLiveTimeline();
@@ -451,7 +451,7 @@ describe("Room", function() {
room.addLiveEvents([events[0]]);
expect(room.timeline.length).toEqual(1);
const firstLiveTimeline = room.getLiveTimeline();
room.resetLiveTimeline();
room.resetLiveTimeline('sometoken', 'someothertoken');
const tl = room.getTimelineForEvent(events[0].getId());
expect(tl).toBe(timelineSupport ? firstLiveTimeline : null);
@@ -886,7 +886,7 @@ describe("Room", function() {
expect(name).toEqual("Empty room");
});
it("should return 'Invite from [inviter display name] if state event " +
it("should return '[inviter display name] if state event " +
"available",
function() {
setJoinRule("invite");
@@ -894,7 +894,7 @@ describe("Room", function() {
addMember(userB, "invite", {user: userA});
room.recalculate(userB);
const name = room.name;
expect(name).toEqual("Invite from Alice");
expect(name).toEqual("Alice");
});
it("should return inviter mxid if display name not available",
@@ -904,7 +904,7 @@ describe("Room", function() {
addMember(userB, "invite", {user: userA});
room.recalculate(userB);
const name = room.name;
expect(name).toEqual("Invite from " + userA);
expect(name).toEqual(userA);
});
});
});
+9 -9
View File
@@ -2,7 +2,7 @@
/* eslint new-cap: "off" */
import 'source-map-support/register';
const q = require("q");
import Promise from 'bluebird';
const sdk = require("../..");
const MatrixScheduler = sdk.MatrixScheduler;
const MatrixError = sdk.MatrixError;
@@ -41,7 +41,7 @@ describe("MatrixScheduler", function() {
});
retryFn = null;
queueFn = null;
defer = q.defer();
defer = Promise.defer();
});
afterEach(function() {
@@ -55,8 +55,8 @@ describe("MatrixScheduler", function() {
queueFn = function() {
return "one_big_queue";
};
const deferA = q.defer();
const deferB = q.defer();
const deferA = Promise.defer();
const deferB = Promise.defer();
let resolvedA = false;
scheduler.setProcessFunction(function(event) {
if (resolvedA) {
@@ -80,7 +80,7 @@ describe("MatrixScheduler", function() {
it("should invoke the retryFn on failure and wait the amount of time specified",
function(done) {
const waitTimeMs = 1500;
const retryDefer = q.defer();
const retryDefer = Promise.defer();
retryFn = function() {
retryDefer.resolve();
return waitTimeMs;
@@ -97,7 +97,7 @@ describe("MatrixScheduler", function() {
return defer.promise;
} else if (procCount === 2) {
// don't care about this defer
return q.defer().promise;
return Promise.defer().promise;
}
expect(procCount).toBeLessThan(3);
});
@@ -125,8 +125,8 @@ describe("MatrixScheduler", function() {
return "yep";
};
const deferA = q.defer();
const deferB = q.defer();
const deferA = Promise.defer();
const deferB = Promise.defer();
let procCount = 0;
scheduler.setProcessFunction(function(ev) {
procCount += 1;
@@ -177,7 +177,7 @@ describe("MatrixScheduler", function() {
const expectOrder = [
eventA.getId(), eventB.getId(), eventD.getId(),
];
const deferA = q.defer();
const deferA = Promise.defer();
scheduler.setProcessFunction(function(event) {
const id = expectOrder.shift();
expect(id).toEqual(event.getId());
+16 -16
View File
@@ -1,6 +1,6 @@
"use strict";
import 'source-map-support/register';
const q = require("q");
import Promise from 'bluebird';
const sdk = require("../..");
const EventTimeline = sdk.EventTimeline;
const TimelineWindow = sdk.TimelineWindow;
@@ -157,7 +157,7 @@ describe("TimelineWindow", function() {
client = {};
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineSet0).toBe(timelineSet);
return q(timeline);
return Promise.resolve(timeline);
};
return new TimelineWindow(client, timelineSet, opts);
@@ -179,7 +179,7 @@ describe("TimelineWindow", function() {
timelineWindow.load(undefined, 2).then(function() {
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) {
@@ -191,14 +191,14 @@ describe("TimelineWindow", function() {
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineSet0).toBe(timelineSet);
expect(eventId0).toEqual(eventId);
return q(timeline);
return Promise.resolve(timeline);
};
const timelineWindow = new TimelineWindow(client, timelineSet);
timelineWindow.load(eventId, 3).then(function() {
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",
@@ -219,7 +219,7 @@ describe("TimelineWindow", function() {
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return q(timeline);
return Promise.resolve(timeline);
};
timelineWindow.load(eventId, 3).then(function() {
@@ -229,7 +229,7 @@ describe("TimelineWindow", function() {
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
});
@@ -277,7 +277,7 @@ 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) {
@@ -322,7 +322,7 @@ 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) {
@@ -367,7 +367,7 @@ 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) {
@@ -383,7 +383,7 @@ 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() {
@@ -399,7 +399,7 @@ describe("TimelineWindow", function() {
expect(success).toBe(true);
const expectedEvents = timeline.getEvents().slice(0, 5);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
@@ -416,7 +416,7 @@ 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() {
@@ -432,7 +432,7 @@ describe("TimelineWindow", function() {
expect(success).toBe(true);
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",
@@ -449,7 +449,7 @@ describe("TimelineWindow", function() {
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() {
@@ -471,7 +471,7 @@ describe("TimelineWindow", function() {
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
}).catch(utils.failTest).done(done);
}).nodeify(done);
});
});
});
+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);
}
}
}
+339 -1
View File
@@ -54,6 +54,8 @@ const utils = require("./utils");
* to all requests with this client. Useful for application services which require
* <code>?user_id=</code>.
*
* @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use
* Authorization header instead of query param to send the access token to the server.
*/
function MatrixBaseApis(opts) {
utils.checkObjectHasKeys(opts, ["baseUrl", "request"]);
@@ -70,6 +72,7 @@ function MatrixBaseApis(opts) {
onlyData: true,
extraParams: opts.queryParams,
localTimeoutMs: opts.localTimeoutMs,
useAuthorizationHeader: opts.useAuthorizationHeader,
};
this._http = new httpApi.MatrixHttpApi(this, httpOpts);
@@ -398,6 +401,304 @@ MatrixBaseApis.prototype.roomState = function(roomId, callback) {
return this._http.authedRequest(callback, "GET", path);
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group summary object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getGroupSummary = function(groupId) {
const path = utils.encodeUri("/groups/$groupId/summary", {$groupId: groupId});
return this._http.authedRequest(undefined, "GET", path);
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group profile object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getGroupProfile = function(groupId) {
const path = utils.encodeUri("/groups/$groupId/profile", {$groupId: groupId});
return this._http.authedRequest(undefined, "GET", path);
};
/**
* @param {string} groupId
* @param {Object} profile The group profile object
* @param {string=} profile.name Name of the group
* @param {string=} profile.avatar_url MXC avatar URL
* @param {string=} profile.short_description A short description of the room
* @param {string=} profile.long_description A longer HTML description of the room
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setGroupProfile = function(groupId, profile) {
const path = utils.encodeUri("/groups/$groupId/profile", {$groupId: groupId});
return this._http.authedRequest(
undefined, "POST", path, undefined, profile,
);
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group users list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getGroupUsers = function(groupId) {
const path = utils.encodeUri("/groups/$groupId/users", {$groupId: groupId});
return this._http.authedRequest(undefined, "GET", path);
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group users list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getGroupInvitedUsers = function(groupId) {
const path = utils.encodeUri("/groups/$groupId/invited_users", {$groupId: groupId});
return this._http.authedRequest(undefined, "GET", path);
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group rooms list object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getGroupRooms = function(groupId) {
const path = utils.encodeUri("/groups/$groupId/rooms", {$groupId: groupId});
return this._http.authedRequest(undefined, "GET", path);
};
/**
* @param {string} groupId
* @param {string} userId
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.inviteUserToGroup = function(groupId, userId) {
const path = utils.encodeUri(
"/groups/$groupId/admin/users/invite/$userId",
{$groupId: groupId, $userId: userId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
};
/**
* @param {string} groupId
* @param {string} userId
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.removeUserFromGroup = function(groupId, userId) {
const path = utils.encodeUri(
"/groups/$groupId/admin/users/remove/$userId",
{$groupId: groupId, $userId: userId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
};
/**
* @param {string} groupId
* @param {string} userId
* @param {string} roleId Optional.
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addUserToGroupSummary = function(groupId, userId, roleId) {
const path = utils.encodeUri(
roleId ?
"/groups/$groupId/summary/$roleId/users/$userId" :
"/groups/$groupId/summary/users/$userId",
{$groupId: groupId, $roleId: roleId, $userId: userId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
};
/**
* @param {string} groupId
* @param {string} userId
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.removeUserFromGroupSummary = function(groupId, userId) {
const path = utils.encodeUri(
"/groups/$groupId/summary/users/$userId",
{$groupId: groupId, $userId: userId},
);
return this._http.authedRequest(undefined, "DELETE", path, undefined, {});
};
/**
* @param {string} groupId
* @param {string} roomId
* @param {string} categoryId Optional.
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addRoomToGroupSummary = function(groupId, roomId, categoryId) {
const path = utils.encodeUri(
categoryId ?
"/groups/$groupId/summary/$categoryId/rooms/$roomId" :
"/groups/$groupId/summary/rooms/$roomId",
{$groupId: groupId, $categoryId: categoryId, $roomId: roomId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
};
/**
* @param {string} groupId
* @param {string} roomId
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.removeRoomFromGroupSummary = function(groupId, roomId) {
const path = utils.encodeUri(
"/groups/$groupId/summary/rooms/$roomId",
{$groupId: groupId, $roomId: roomId},
);
return this._http.authedRequest(undefined, "DELETE", path, undefined, {});
};
/**
* @param {string} groupId
* @param {string} roomId
* @param {bool} isPublic Whether the room-group association is visible to non-members
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addRoomToGroup = function(groupId, roomId, isPublic) {
if (isPublic === undefined) {
isPublic = true;
}
const path = utils.encodeUri(
"/groups/$groupId/admin/rooms/$roomId",
{$groupId: groupId, $roomId: roomId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined,
{ "m.visibility": { type: isPublic ? "public" : "private" } },
);
};
/**
* Configure the visibility of a room-group association.
* @param {string} groupId
* @param {string} roomId
* @param {bool} isPublic Whether the room-group association is visible to non-members
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.updateGroupRoomVisibility = function(groupId, roomId, isPublic) {
// NB: The /config API is generic but there's not much point in exposing this yet as synapse
// is the only server to implement this. In future we should consider an API that allows
// arbitrary configuration, i.e. "config/$configKey".
const path = utils.encodeUri(
"/groups/$groupId/admin/rooms/$roomId/config/m.visibility",
{$groupId: groupId, $roomId: roomId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined,
{ type: isPublic ? "public" : "private" },
);
};
/**
* @param {string} groupId
* @param {string} roomId
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.removeRoomFromGroup = function(groupId, roomId) {
const path = utils.encodeUri(
"/groups/$groupId/admin/rooms/$roomId",
{$groupId: groupId, $roomId: roomId},
);
return this._http.authedRequest(undefined, "DELETE", path, undefined, {});
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.acceptGroupInvite = function(groupId) {
const path = utils.encodeUri(
"/groups/$groupId/self/accept_invite",
{$groupId: groupId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.leaveGroup = function(groupId) {
const path = utils.encodeUri(
"/groups/$groupId/self/leave",
{$groupId: groupId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
};
/**
* @return {module:client.Promise} Resolves: The groups to which the user is joined
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getJoinedGroups = function() {
const path = utils.encodeUri("/joined_groups");
return this._http.authedRequest(undefined, "GET", path);
};
/**
* @param {Object} content Request content
* @param {string} content.localpart The local part of the desired group ID
* @param {Object} content.profile Group profile object
* @return {module:client.Promise} Resolves: Object with key group_id: id of the created group
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.createGroup = function(content) {
const path = utils.encodeUri("/create_group");
return this._http.authedRequest(
undefined, "POST", path, undefined, content,
);
};
/**
* @param {string[]} userIds List of user IDs
* @return {module:client.Promise} Resolves: Object as exmaple below
*
* {
* "users": {
* "@bob:example.com": {
* "+example:example.com"
* }
* }
* }
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getPublicisedGroups = function(userIds) {
const path = utils.encodeUri("/publicised_groups");
return this._http.authedRequest(
undefined, "POST", path, undefined, { user_ids: userIds },
);
};
/**
* @param {string} groupId
* @param {bool} isPublic Whether the user's membership of this group is made public
* @return {module:client.Promise} Resolves: Empty object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setGroupPublicity = function(groupId, isPublic) {
const path = utils.encodeUri(
"/groups/$groupId/self/update_publicity",
{$groupId: groupId},
);
return this._http.authedRequest(undefined, "PUT", path, undefined, {
publicise: isPublic,
});
};
/**
* Retrieve a state event.
* @param {string} roomId
@@ -678,6 +979,31 @@ MatrixBaseApis.prototype.setRoomDirectoryVisibilityAppService =
);
};
// User Directory Operations
// =========================
/**
* Query the user directory with a term matching user IDs, display names and domains.
* @param {object} opts options
* @param {string} opts.term the term with which to search.
* @param {number} opts.limit the maximum number of results to return. The server will
* apply a limit if unspecified.
* @return {module:client.Promise} Resolves: an array of results.
*/
MatrixBaseApis.prototype.searchUserDirectory = function(opts) {
const body = {
search_term: opts.term,
};
if (opts.limit !== undefined) {
body.limit = opts.limit;
}
return this._http.authedRequest(
undefined, "POST", "/user_directory/search", undefined, body,
);
};
// Media operations
// ================
@@ -710,6 +1036,10 @@ MatrixBaseApis.prototype.setRoomDirectoryVisibilityAppService =
* invoke on success/failure. See the promise return values for more
* information.
*
* @param {Function=} opts.progressHandler Optional. Called when a chunk of
* data has been uploaded, with an object containing the fields `loaded`
* (number of bytes transferred) and `total` (total size, if known).
*
* @return {module:client.Promise} Resolves to response object, as
* determined by this.opts.onlyData, opts.rawResponse, and
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
@@ -1301,7 +1631,15 @@ MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
return this._http.authedRequestWithPrefix(
undefined, "GET", "/thirdparty/protocols", undefined, undefined,
httpApi.PREFIX_UNSTABLE,
);
).then((response) => {
// sanity check
if (!response || typeof(response) !== 'object') {
throw new Error(
`/thirdparty/protocols did not return an object: ${response}`,
);
}
return response;
});
};
/**
+374 -128
View File
@@ -23,7 +23,7 @@ const PushProcessor = require('./pushprocessor');
* @module client
*/
const EventEmitter = require("events").EventEmitter;
const q = require("q");
import Promise from 'bluebird';
const url = require('url');
const httpApi = require("./http-api");
@@ -40,6 +40,8 @@ const SyncApi = require("./sync");
const MatrixBaseApis = require("./base-apis");
const MatrixError = httpApi.MatrixError;
import ReEmitter from './ReEmitter';
const SCROLLBACK_DELAY_MS = 3000;
let CRYPTO_ENABLED = false;
@@ -47,8 +49,7 @@ try {
var Crypto = require("./crypto");
CRYPTO_ENABLED = true;
} catch (e) {
console.error("olm load error", e);
// Olm not installed.
console.warn("Unable to load crypto module: crypto will be disabled: " + e);
}
/**
@@ -98,16 +99,34 @@ try {
* @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of
* time to wait before timing out HTTP requests. If not specified, there is no timeout.
*
* @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use
* Authorization header instead of query param to send the access token to the server.
*
* @param {boolean} [opts.timelineSupport = false] Set to true to enable
* improved timeline support ({@link
* module:client~MatrixClient#getEventTimeline getEventTimeline}). It is
* disabled by default for compatibility with older clients - in particular to
* maintain support for back-paginating the live timeline after a '/sync'
* result with a gap.
*
* @param {module:crypto.store.base~CryptoStore} opts.cryptoStore
* crypto store implementation.
*/
function MatrixClient(opts) {
// Allow trailing slash in HS url
if (opts.baseUrl && opts.baseUrl.endsWith("/")) {
opts.baseUrl = opts.baseUrl.substr(0, opts.baseUrl.length - 1);
}
// Allow trailing slash in IS url
if (opts.idBaseUrl && opts.idBaseUrl.endsWith("/")) {
opts.idBaseUrl = opts.idBaseUrl.substr(0, opts.idBaseUrl.length - 1);
}
MatrixBaseApis.call(this, opts);
this.reEmitter = new ReEmitter(this);
this.store = opts.store || new StubStore();
this.deviceId = opts.deviceId || null;
@@ -153,21 +172,49 @@ function MatrixClient(opts) {
this._notifTimelineSet = null;
this._crypto = null;
if (CRYPTO_ENABLED && Boolean(opts.sessionStore) &&
userId !== null && this.deviceId !== null) {
this._crypto = new Crypto(
this, this,
opts.sessionStore,
userId, this.deviceId,
this.store,
);
this._cryptoStore = opts.cryptoStore;
this._sessionStore = opts.sessionStore;
this._forceTURN = opts.forceTURN || false;
if (CRYPTO_ENABLED) {
this.olmVersion = Crypto.getOlmVersion();
}
}
utils.inherits(MatrixClient, EventEmitter);
utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype);
/**
* Clear any data out of the persistent stores used by the client.
*
* @returns {Promise} Promise which resolves when the stores have been cleared.
*/
MatrixClient.prototype.clearStores = function() {
if (this._clientRunning) {
throw new Error("Cannot clear stores while client is running");
}
const promises = [];
promises.push(this.store.deleteAllData());
if (this._cryptoStore) {
promises.push(this._cryptoStore.deleteAllData());
}
return Promise.all(promises);
};
/**
* Get the user-id of the logged-in user
*
* @return {?string} MXID for the logged-in user, or null if not logged in
*/
MatrixClient.prototype.getUserId = function() {
if (this.credentials && this.credentials.userId) {
return this.credentials.userId;
}
return null;
};
/**
* Get the domain for this client's MXID
* @return {?string} Domain of this MXID
@@ -207,6 +254,16 @@ MatrixClient.prototype.supportsVoip = function() {
return this._supportsVoip;
};
/**
* Set whether VoIP calls are forced to use only TURN
* candidates. This is the same as the forceTURN option
* when creating the client.
* @param {bool} forceTURN True to force use of TURN servers
*/
MatrixClient.prototype.setForceTURN = function(forceTURN) {
this._forceTURN = forceTURN;
};
/**
* Get the current sync state.
* @return {?string} the sync state, which may be null.
@@ -278,6 +335,73 @@ MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) {
// Crypto bits
// ===========
/**
* Initialise support for end-to-end encryption in this client
*
* You should call this method after creating the matrixclient, but *before*
* calling `startClient`, if you want to support end-to-end encryption.
*
* It will return a Promise which will resolve when the crypto layer has been
* successfully initialised.
*/
MatrixClient.prototype.initCrypto = async function() {
if (this._crypto) {
console.warn("Attempt to re-initialise e2e encryption on MatrixClient");
return;
}
if (!CRYPTO_ENABLED) {
throw new Error(
`End-to-end encryption not supported in this js-sdk build: did ` +
`you remember to load the olm library?`,
);
}
if (!this._sessionStore) {
// this is temporary, the sessionstore is supposed to be going away
throw new Error(`Cannot enable encryption: no sessionStore provided`);
}
if (!this._cryptoStore) {
// the cryptostore is provided by sdk.createClient, so this shouldn't happen
throw new Error(`Cannot enable encryption: no cryptoStore provided`);
}
const userId = this.getUserId();
if (userId === null) {
throw new Error(
`Cannot enable encryption on MatrixClient with unknown userId: ` +
`ensure userId is passed in createClient().`,
);
}
if (this.deviceId === null) {
throw new Error(
`Cannot enable encryption on MatrixClient with unknown deviceId: ` +
`ensure deviceId is passed in createClient().`,
);
}
const crypto = new Crypto(
this,
this._sessionStore,
userId, this.deviceId,
this.store,
this._cryptoStore,
);
this.reEmitter.reEmit(crypto, [
"crypto.roomKeyRequest",
"crypto.roomKeyRequestCancellation",
]);
await crypto.init();
// if crypto initialisation was successful, tell it to attach its event
// handlers.
crypto.registerEventHandlers(this);
this._crypto = crypto;
};
/**
* Is end-to-end crypto enabled for this client.
* @return {boolean} True if end-to-end is enabled.
@@ -323,42 +447,40 @@ MatrixClient.prototype.uploadKeys = function() {
*/
MatrixClient.prototype.downloadKeys = function(userIds, forceDownload) {
if (this._crypto === null) {
return q.reject(new Error("End-to-end encryption disabled"));
return Promise.reject(new Error("End-to-end encryption disabled"));
}
return this._crypto.downloadKeys(userIds, forceDownload);
};
/**
* List the stored device keys for a user id
*
* @deprecated prefer {@link module:client#getStoredDevicesForUser}
*
* @param {string} userId the user to list keys for.
*
* @return {object[]} list of devices with "id", "verified", "blocked",
* "key", and "display_name" parameters.
*/
MatrixClient.prototype.listDeviceKeys = function(userId) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.listDeviceKeys(userId);
};
/**
* 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
* @return {Promise<module:crypto-deviceinfo[]>} list of devices
*/
MatrixClient.prototype.getStoredDevicesForUser = function(userId) {
MatrixClient.prototype.getStoredDevicesForUser = async function(userId) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.getStoredDevicesForUser(userId) || [];
};
/**
* Get the stored device key for a user id and device id
*
* @param {string} userId the user to list keys for.
* @param {string} deviceId unique identifier for the device
*
* @return {Promise<?module:crypto-deviceinfo>} device or null
*/
MatrixClient.prototype.getStoredDevice = async function(userId, deviceId) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
return this._crypto.getStoredDevice(userId, deviceId) || null;
};
/**
* Mark the given device as verified
*
@@ -368,13 +490,15 @@ MatrixClient.prototype.getStoredDevicesForUser = function(userId) {
* @param {boolean=} verified whether to mark the device as verified. defaults
* to 'true'.
*
* @returns {Promise}
*
* @fires module:client~event:MatrixClient"deviceVerificationChanged"
*/
MatrixClient.prototype.setDeviceVerified = function(userId, deviceId, verified) {
if (verified === undefined) {
verified = true;
}
_setDeviceVerification(this, userId, deviceId, verified, null);
return _setDeviceVerification(this, userId, deviceId, verified, null);
};
/**
@@ -386,13 +510,15 @@ MatrixClient.prototype.setDeviceVerified = function(userId, deviceId, verified)
* @param {boolean=} blocked whether to mark the device as blocked. defaults
* to 'true'.
*
* @returns {Promise}
*
* @fires module:client~event:MatrixClient"deviceVerificationChanged"
*/
MatrixClient.prototype.setDeviceBlocked = function(userId, deviceId, blocked) {
if (blocked === undefined) {
blocked = true;
}
_setDeviceVerification(this, userId, deviceId, null, blocked);
return _setDeviceVerification(this, userId, deviceId, null, blocked);
};
/**
@@ -404,20 +530,24 @@ MatrixClient.prototype.setDeviceBlocked = function(userId, deviceId, blocked) {
* @param {boolean=} known whether to mark the device as known. defaults
* to 'true'.
*
* @returns {Promise}
*
* @fires module:client~event:MatrixClient"deviceVerificationChanged"
*/
MatrixClient.prototype.setDeviceKnown = function(userId, deviceId, known) {
if (known === undefined) {
known = true;
}
_setDeviceVerification(this, userId, deviceId, null, null, known);
return _setDeviceVerification(this, userId, deviceId, null, null, known);
};
function _setDeviceVerification(client, userId, deviceId, verified, blocked, known) {
async function _setDeviceVerification(
client, userId, deviceId, verified, blocked, known,
) {
if (!client._crypto) {
throw new Error("End-to-End encryption disabled");
}
const dev = client._crypto.setDeviceVerification(
const dev = await client._crypto.setDeviceVerification(
userId, deviceId, verified, blocked, known,
);
client.emit("deviceVerificationChanged", userId, deviceId, dev);
@@ -454,9 +584,9 @@ MatrixClient.prototype.getGlobalBlacklistUnverifiedDevices = function() {
*
* @param {MatrixEvent} event event to be checked
*
* @return {module:crypto/deviceinfo?}
* @return {Promise<module:crypto/deviceinfo?>}
*/
MatrixClient.prototype.getEventSenderDeviceInfo = function(event) {
MatrixClient.prototype.getEventSenderDeviceInfo = async function(event) {
if (!this._crypto) {
return null;
}
@@ -472,8 +602,8 @@ MatrixClient.prototype.getEventSenderDeviceInfo = function(event) {
* @return {boolean} true if the sender of this event has been verified using
* {@link module:client~MatrixClient#setDeviceVerified|setDeviceVerified}.
*/
MatrixClient.prototype.isEventSenderVerified = function(event) {
const device = this.getEventSenderDeviceInfo(event);
MatrixClient.prototype.isEventSenderVerified = async function(event) {
const device = await this.getEventSenderDeviceInfo(event);
if (!device) {
return false;
}
@@ -484,14 +614,13 @@ MatrixClient.prototype.isEventSenderVerified = function(event) {
* Enable end-to-end encryption for a room.
* @param {string} roomId The room ID to enable encryption in.
* @param {object} config The encryption config for the room.
* @return {Object} A promise that will resolve when encryption is setup.
* @return {Promise} A promise that will resolve when encryption is set up.
*/
MatrixClient.prototype.setRoomEncryption = function(roomId, config) {
if (!this._crypto) {
throw new Error("End-to-End encryption disabled");
}
this._crypto.setRoomEncryption(roomId, config);
return q();
return this._crypto.setRoomEncryption(roomId, config);
};
/**
@@ -500,11 +629,28 @@ MatrixClient.prototype.setRoomEncryption = function(roomId, config) {
* @return {bool} whether encryption is enabled.
*/
MatrixClient.prototype.isRoomEncrypted = function(roomId) {
if (!this._crypto) {
const room = this.getRoom(roomId);
if (!room) {
// we don't know about this room, so can't determine if it should be
// encrypted. Let's assume not.
return false;
}
return this._crypto.isRoomEncrypted(roomId);
// if there is an 'm.room.encryption' event in this room, it should be
// encrypted (independently of whether we actually support encryption)
const ev = room.currentState.getStateEvents("m.room.encryption", "");
if (ev) {
return true;
}
// we don't have an m.room.encrypted event, but that might be because
// the server is hiding it from us. Check the store to see if it was
// previously encrypted.
if (!this._sessionStore) {
return false;
}
return Boolean(this._sessionStore.getEndToEndRoom(roomId));
};
/**
@@ -517,7 +663,7 @@ MatrixClient.prototype.isRoomEncrypted = function(roomId) {
*/
MatrixClient.prototype.exportRoomKeys = function() {
if (!this._crypto) {
return q.reject(new Error("End-to-end encryption disabled"));
return Promise.reject(new Error("End-to-end encryption disabled"));
}
return this._crypto.exportRoomKeys();
};
@@ -526,49 +672,40 @@ MatrixClient.prototype.exportRoomKeys = function() {
* Import a list of room keys previously exported by exportRoomKeys
*
* @param {Object[]} keys a list of session export objects
*
* @return {module:client.Promise} a promise which resolves when the keys
* have been imported
*/
MatrixClient.prototype.importRoomKeys = function(keys) {
if (!this._crypto) {
throw new Error("End-to-end encryption disabled");
}
this._crypto.importRoomKeys(keys);
return this._crypto.importRoomKeys(keys);
};
// Group ops
// =========
// Operations on groups that come down the sync stream (ie. ones the
// user is a member of or invited to)
/**
* Get the group for the given group ID.
* This function will return a valid group for any group for which a Group event
* has been emitted.
* @param {string} groupId The group ID
* @return {Group} The Group or null if the group is not known or there is no data store.
*/
MatrixClient.prototype.getGroup = function(groupId) {
return this.store.getGroup(groupId);
};
/**
* Decrypt a received event according to the algorithm specified in the event.
*
* @param {MatrixClient} client
* @param {MatrixEvent} event
* Retrieve all known groups.
* @return {Groups[]} A list of groups, or an empty list if there is no data store.
*/
function _decryptEvent(client, event) {
if (!client._crypto) {
_badEncryptedMessage(event, "Encryption not enabled");
return;
}
try {
client._crypto.decryptEvent(event);
} catch (e) {
console.warn(
`Error decrypting event (id=${event.getId()}): ${e}`,
);
if (!(e instanceof Crypto.DecryptionError)) {
throw e;
}
_badEncryptedMessage(event, e.message);
return;
}
}
function _badEncryptedMessage(event, reason) {
event.setClearData({
type: "m.room.message",
content: {
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: " + reason + " **",
},
});
}
MatrixClient.prototype.getGroups = function() {
return this.store.getGroups();
};
// Room ops
// ========
@@ -642,6 +779,38 @@ MatrixClient.prototype.getAccountData = function(eventType) {
return this.store.getAccountData(eventType);
};
/**
* Gets the users that are ignored by this client
* @returns {string[]} The array of users that are ignored (empty if none)
*/
MatrixClient.prototype.getIgnoredUsers = function() {
const event = this.getAccountData("m.ignored_user_list");
if (!event || !event.getContent() || !event.getContent()["ignored_users"]) return [];
return Object.keys(event.getContent()["ignored_users"]);
};
/**
* Sets the users that the current user should ignore.
* @param {string[]} userIds the user IDs to ignore
* @param {module:client.callback} [callback] Optional.
* @return {module:client.Promise} Resolves: Account data event
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setIgnoredUsers = function(userIds, callback) {
const content = {ignored_users: {}};
userIds.map((u) => content.ignored_users[u] = {});
return this.setAccountData("m.ignored_user_list", content, callback);
};
/**
* Gets whether or not a specific user is being ignored by this client.
* @param {string} userId the user ID to check
* @returns {boolean} true if the user is ignored, false otherwise
*/
MatrixClient.prototype.isUserIgnored = function(userId) {
return this.getIgnoredUsers().indexOf(userId) !== -1;
};
// Room operations
// ===============
@@ -670,10 +839,10 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
const room = this.getRoom(roomIdOrAlias);
if (room && room.hasMembershipState(this.credentials.userId, "join")) {
return q(room);
return Promise.resolve(room);
}
let sign_promise = q();
let sign_promise = Promise.resolve();
if (opts.inviteSignUrl) {
sign_promise = this._http.requestOtherUrl(
@@ -682,7 +851,7 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
);
}
const defer = q.defer();
const defer = Promise.defer();
const self = this;
sign_promise.then(function(signed_invite_object) {
@@ -701,7 +870,7 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) {
// v2 will do this for us
// return syncApi.syncRoom(room);
}
return q(room);
return Promise.resolve(room);
}).done(function(room) {
_resolve(callback, defer, room);
}, function(err) {
@@ -890,6 +1059,8 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
txnId = this.makeTxnId();
}
console.log(`sendEvent of type ${eventType} in ${roomId} with txnId ${txnId}`);
// we always construct a MatrixEvent when sending because the store and
// scheduler use them. We'll extract the params back out if it turns out
// the client has no scheduler or store.
@@ -919,21 +1090,20 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId,
// marks the event as sent/unsent
// returns a promise which resolves with the result of the send request
function _sendEvent(client, room, event, callback) {
// Add an extra q() to turn synchronous exceptions into promise rejections,
// Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections,
// so that we can handle synchronous and asynchronous exceptions with the
// same code path.
return q().then(function() {
let encryptionPromise = null;
if (client._crypto) {
encryptionPromise = client._crypto.encryptEventIfNeeded(event, room);
return Promise.resolve().then(function() {
const encryptionPromise = _encryptEventIfNeeded(client, event, room);
if (!encryptionPromise) {
return null;
}
if (encryptionPromise) {
_updatePendingEventStatus(room, event, EventStatus.ENCRYPTING);
encryptionPromise = encryptionPromise.then(function() {
_updatePendingEventStatus(room, event, EventStatus.SENDING);
});
}
return encryptionPromise;
_updatePendingEventStatus(room, event, EventStatus.ENCRYPTING);
return encryptionPromise.then(() => {
_updatePendingEventStatus(room, event, EventStatus.SENDING);
});
}).then(function() {
let promise;
// this event may be queued
@@ -980,6 +1150,43 @@ function _sendEvent(client, room, event, callback) {
});
}
/**
* Encrypt an event according to the configuration of the room, if necessary.
*
* @param {MatrixClient} client
*
* @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
*/
function _encryptEventIfNeeded(client, 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 (!client.isRoomEncrypted(event.getRoomId())) {
// looks like this room isn't encrypted.
return null;
}
if (!client._crypto) {
throw new Error(
"This room is configured to use encryption, but your client does " +
"not support encryption.",
);
}
return client._crypto.encryptEvent(event, room);
}
function _updatePendingEventStatus(room, event, newStatus) {
if (room) {
room.updatePendingEvent(event, newStatus);
@@ -1014,7 +1221,12 @@ function _sendEventHttpRequest(client, event) {
return client._http.authedRequest(
undefined, "PUT", path, undefined, event.getWireContent(),
);
).then((res) => {
console.log(
`Event sent to ${event.getRoomId()} with event id ${res.event_id}`,
);
return res;
});
}
/**
@@ -1171,7 +1383,7 @@ MatrixClient.prototype.sendHtmlEmote = function(roomId, body, htmlBody, callback
*/
MatrixClient.prototype.sendReceipt = function(event, receiptType, callback) {
if (this.isGuest()) {
return q({}); // guests cannot send receipts so don't bother.
return Promise.resolve({}); // guests cannot send receipts so don't bother.
}
const path = utils.encodeUri("/rooms/$roomId/receipt/$receiptType/$eventId", {
@@ -1248,7 +1460,7 @@ MatrixClient.prototype.getUrlPreview = function(url, ts, callback) {
const key = ts + "_" + url;
const og = this.urlPreviewCache[key];
if (og) {
return q(og);
return Promise.resolve(og);
}
const self = this;
@@ -1274,7 +1486,7 @@ MatrixClient.prototype.getUrlPreview = function(url, ts, callback) {
*/
MatrixClient.prototype.sendTyping = function(roomId, isTyping, timeoutMs, callback) {
if (this.isGuest()) {
return q({}); // guests cannot send typing notifications so don't bother.
return Promise.resolve({}); // guests cannot send typing notifications so don't bother.
}
const path = utils.encodeUri("/rooms/$roomId/typing/$userId", {
@@ -1335,7 +1547,7 @@ MatrixClient.prototype.inviteByThreePid = function(roomId, medium, address, call
let identityServerUrl = this.getIdentityServerUrl();
if (!identityServerUrl) {
return q.reject(new MatrixError({
return Promise.reject(new MatrixError({
error: "No supplied identity server URL",
errcode: "ORG.MATRIX.JSSDK_MISSING_PARAM",
}));
@@ -1675,13 +1887,13 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) {
}
if (room.oldState.paginationToken === null) {
return q(room); // already at the start.
return Promise.resolve(room); // already at the start.
}
// attempt to grab more events from the store first
const numAdded = this.store.scrollback(room, limit).length;
if (numAdded === limit) {
// store contained everything we needed.
return q(room);
return Promise.resolve(room);
}
// reduce the required number of events appropriately
limit = limit - numAdded;
@@ -1694,7 +1906,7 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) {
limit: limit,
dir: 'b',
};
const defer = q.defer();
const defer = Promise.defer();
info = {
promise: defer.promise,
errorTs: null,
@@ -1702,7 +1914,7 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) {
const self = this;
// wait for a time before doing this request
// (which may be 0 in order not to special case the code paths)
q.delay(timeToWaitMs).then(function() {
Promise.delay(timeToWaitMs).then(function() {
return self._http.authedRequest(callback, "GET", path, params);
}).done(function(res) {
const matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self));
@@ -1745,7 +1957,7 @@ MatrixClient.prototype.paginateEventContext = function(eventContext, opts) {
const token = eventContext.getPaginateToken(backwards);
if (!token) {
// no more results.
return q.reject(new Error("No paginate token"));
return Promise.reject(new Error("No paginate token"));
}
const dir = backwards ? 'b' : 'f';
@@ -1814,7 +2026,7 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) {
}
if (timelineSet.getTimelineForEvent(eventId)) {
return q(timelineSet.getTimelineForEvent(eventId));
return Promise.resolve(timelineSet.getTimelineForEvent(eventId));
}
const path = utils.encodeUri(
@@ -1902,7 +2114,7 @@ MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) {
const token = eventTimeline.getPaginationToken(dir);
if (!token) {
// no token - no results.
return q(false);
return Promise.resolve(false);
}
const pendingRequest = eventTimeline._paginationRequests[dir];
@@ -2022,7 +2234,7 @@ MatrixClient.prototype.resetNotifTimelineSet = function() {
// know about /notifications, so we have no choice but to start paginating
// from the current point in time. This may well overlap with historical
// notifs which are then inserted into the timeline by /sync responses.
this._notifTimelineSet.resetLiveTimeline('end', true);
this._notifTimelineSet.resetLiveTimeline('end', null);
// we could try to paginate a single event at this point in order to get
// a more valid pagination token, but it just ends up with an out of order
@@ -2079,14 +2291,14 @@ MatrixClient.prototype.setGuestAccess = function(roomId, opts) {
guest_access: opts.allowJoin ? "can_join" : "forbidden",
});
let readPromise = q();
let readPromise = Promise.resolve();
if (opts.allowRead) {
readPromise = this.sendStateEvent(roomId, "m.room.history_visibility", {
history_visibility: "world_readable",
});
}
return q.all(readPromise, writePromise);
return Promise.all([readPromise, writePromise]);
};
// Registration/Login operations
@@ -2354,7 +2566,7 @@ MatrixClient.prototype.setRoomMutePushRule = function(scope, roomId, mute) {
} else if (!hasDontNotifyRule) {
// Remove the existing one before setting the mute push rule
// This is a workaround to SYN-590 (Push rule update fails)
deferred = q.defer();
deferred = Promise.defer();
this.deletePushRule(scope, "room", roomPushRule.rule_id)
.done(function() {
self.addPushRule(scope, "room", roomId, {
@@ -2374,7 +2586,7 @@ MatrixClient.prototype.setRoomMutePushRule = function(scope, roomId, mute) {
if (deferred) {
// Update this.pushRules when the operation completes
const ruleRefreshDeferred = q.defer();
const ruleRefreshDeferred = Promise.defer();
deferred.done(function() {
self.getPushRules().done(function(result) {
self.pushRules = result;
@@ -2488,7 +2700,7 @@ MatrixClient.prototype.backPaginateRoomEventsSearch = function(searchResults) {
// nicely with HTTP errors.
if (!searchResults.next_batch) {
return q.reject(new Error("Cannot backpaginate event search any further"));
return Promise.reject(new Error("Cannot backpaginate event search any further"));
}
if (searchResults.pendingRequest) {
@@ -2557,7 +2769,7 @@ MatrixClient.prototype._processRoomEventsSearch = function(searchResults, respon
MatrixClient.prototype.syncLeftRooms = function() {
// Guard against multiple calls whilst ongoing and multiple calls post success
if (this._syncedLeftRooms) {
return q([]); // don't call syncRooms again if it succeeded.
return Promise.resolve([]); // don't call syncRooms again if it succeeded.
}
if (this._syncLeftRoomsPromise) {
return this._syncLeftRoomsPromise; // return the ongoing request
@@ -2616,7 +2828,7 @@ MatrixClient.prototype.getFilter = function(userId, filterId, allowCached) {
if (allowCached) {
const filter = this.store.getFilter(userId, filterId);
if (filter) {
return q(filter);
return Promise.resolve(filter);
}
}
@@ -2645,7 +2857,7 @@ MatrixClient.prototype.getFilter = function(userId, filterId, allowCached) {
*/
MatrixClient.prototype.getOrCreateFilter = function(filterName, filter) {
const filterId = this.store.getFilterIdByName(filterName);
let promise = q();
let promise = Promise.resolve();
const self = this;
if (filterId) {
@@ -2660,7 +2872,7 @@ MatrixClient.prototype.getOrCreateFilter = function(filterName, filter) {
// super, just use that.
// debuglog("Using existing filter ID %s: %s", filterId,
// JSON.stringify(oldDef));
return q(filterId);
return Promise.resolve(filterId);
}
// debuglog("Existing filter ID %s: %s; new filter: %s",
// filterId, JSON.stringify(oldDef), JSON.stringify(newDef));
@@ -2799,6 +3011,7 @@ MatrixClient.prototype.startClient = function(opts) {
if (this._crypto) {
this._crypto.uploadDeviceKeys().done();
this._crypto.start();
}
// periodically poll for turn servers if we support voip
@@ -2831,12 +3044,20 @@ MatrixClient.prototype.startClient = function(opts) {
* clean shutdown.
*/
MatrixClient.prototype.stopClient = function() {
console.log('stopping MatrixClient');
this.clientRunning = false;
// TODO: f.e. Room => self.store.storeRoom(room) ?
if (this._syncApi) {
this._syncApi.stop();
this._syncApi = null;
}
if (this._crypto) {
this._crypto.stop();
}
if (this._peekSync) {
this._peekSync.stopPeeking();
}
global.clearTimeout(this._checkTurnServersTimeoutID);
};
@@ -2888,6 +3109,10 @@ function setupCallEventHandler(client) {
// now loop through the buffer chronologically and inject them
callEventBuffer.forEach(function(e) {
if (ignoreCallIds[e.getContent().call_id]) {
console.log(
'Ignoring previously answered/hungup call ' +
e.getContent().call_id,
);
return;
}
callEventHandler(e);
@@ -2896,20 +3121,25 @@ function setupCallEventHandler(client) {
}
});
client.on("event", function(event) {
if (!isClientPrepared) {
if (event.getType().indexOf("m.call.") === 0) {
callEventBuffer.push(event);
client.on("event", onEvent);
function onEvent(event) {
if (event.getType().indexOf("m.call.") !== 0) {
// not a call event
if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
// not *yet* a call event, but might become one...
event.once("Event.decrypted", onEvent);
}
return;
}
if (!isClientPrepared) {
callEventBuffer.push(event);
return;
}
callEventHandler(event);
});
}
function callEventHandler(event) {
if (event.getType().indexOf("m.call.") !== 0) {
return; // not a call event
}
const content = event.getContent();
let call = content.call_id ? client.callList[content.call_id] : undefined;
let i;
@@ -2935,7 +3165,9 @@ function setupCallEventHandler(client) {
);
}
call = webRtcCall.createNewMatrixCall(client, event.getRoomId());
call = webRtcCall.createNewMatrixCall(client, event.getRoomId(), {
forceTURN: client._forceTURN,
});
if (!call) {
console.log(
"Incoming call ID " + content.call_id + " but this client " +
@@ -3101,7 +3333,10 @@ function _PojoToMatrixEventMapper(client) {
function mapper(plainOldJsObject) {
const event = new MatrixEvent(plainOldJsObject);
if (event.isEncrypted()) {
_decryptEvent(client, event);
client.reEmitter.reEmit(event, [
"Event.decrypted",
]);
event.attemptDecryption(client._crypto);
}
return event;
}
@@ -3282,6 +3517,17 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
* });
*/
/**
* Fires whenever the sdk learns about a new group. <strong>This event
* is experimental and may change.</strong>
* @event module:client~MatrixClient#"Group"
* @param {Group} group The newly created, fully populated group.
* @example
* matrixClient.on("Group", function(group){
* var groupId = group.groupId;
* });
*/
/**
* Fires whenever a new Room is added. This will fire when you are invited to a
* room, as well as when you join a room. <strong>This event is experimental and
+65 -18
View File
@@ -21,13 +21,35 @@ limitations under the License.
* Manages the list of other users' devices
*/
import q from 'q';
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_NOT_TRACKED = 0;
const TRACKING_STATUS_PENDING_DOWNLOAD = 1;
const TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2;
const TRACKING_STATUS_UP_TO_DATE = 3;
@@ -76,6 +98,10 @@ export default class DeviceList {
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);
@@ -88,7 +114,11 @@ export default class DeviceList {
promises.push(downloadPromise);
}
return q.all(promises).then(() => {
if (promises.length === 0) {
console.log("downloadKeys: already have all necessary keys");
}
return Promise.all(promises).then(() => {
return this._getDevicesFromStore(userIds);
});
}
@@ -228,6 +258,26 @@ export default class DeviceList {
// 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.
*
@@ -275,9 +325,6 @@ export default class DeviceList {
usersToDownload.push(userId);
}
}
if (usersToDownload.length == 0) {
return;
}
// we didn't persist the tracking status during
// invalidateUserDeviceList, so do it now.
@@ -301,7 +348,7 @@ export default class DeviceList {
_doKeyDownload(users) {
if (users.length === 0) {
// nothing to do
return q();
return Promise.resolve();
}
const prom = this._serialiser.updateDevicesForUsers(
@@ -408,7 +455,7 @@ class DeviceListUpdateSerialiser {
this._nextSyncToken = syncToken;
if (!this._queuedQueryDeferred) {
this._queuedQueryDeferred = q.defer();
this._queuedQueryDeferred = Promise.defer();
}
if (this._downloadInProgress) {
@@ -451,10 +498,10 @@ class DeviceListUpdateSerialiser {
//
// of course we ought to do this in a web worker or similar, but
// this serves as an easy solution for now.
let prom = q();
let prom = Promise.resolve();
for (const userId of downloadUsers) {
prom = prom.delay(5).then(() => {
this._processQueryResponseForUser(userId, dk[userId]);
return this._processQueryResponseForUser(userId, dk[userId]);
});
}
@@ -471,14 +518,14 @@ class DeviceListUpdateSerialiser {
}
}, (e) => {
console.warn('Error downloading keys for ' + downloadUsers + ':', e);
this._downloadInProgressInProgress = false;
this._downloadInProgress = false;
deferred.reject(e);
});
return deferred.promise;
}
_processQueryResponseForUser(userId, response) {
async _processQueryResponseForUser(userId, response) {
console.log('got keys for ' + userId + ':', response);
// map from deviceid -> deviceinfo for this user
@@ -491,7 +538,7 @@ class DeviceListUpdateSerialiser {
});
}
_updateStoredDeviceKeysForUser(
await _updateStoredDeviceKeysForUser(
this._olmDevice, userId, userStore, response || {},
);
@@ -508,7 +555,7 @@ class DeviceListUpdateSerialiser {
}
function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
userResult) {
let updated = false;
@@ -546,7 +593,7 @@ function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
continue;
}
if (_storeDeviceKeys(_olmDevice, userStore, deviceResult)) {
if (await _storeDeviceKeys(_olmDevice, userStore, deviceResult)) {
updated = true;
}
}
@@ -557,9 +604,9 @@ function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
/*
* Process a device in a /query response, and add it to the userStore
*
* returns true if a change was made, else false
* returns (a promise for) true if a change was made, else false
*/
function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
if (!deviceResult.keys) {
// no keys?
return false;
@@ -579,7 +626,7 @@ function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
const unsigned = deviceResult.unsigned || {};
try {
olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
} catch (e) {
console.warn("Unable to verify signature on device " +
userId + ":" + deviceId + ":" + e);
+215 -84
View File
@@ -20,11 +20,9 @@ limitations under the License.
*
* @module crypto/OlmDevice
*/
const Olm = require("olm");
const Olm = global.Olm;
if (!Olm) {
// this happens if we were loaded via browserify and the Olm module was not
// loaded.
throw new Error("Olm is not defined");
throw new Error("global.Olm is not defined");
}
const utils = require("../utils");
@@ -57,6 +55,8 @@ function checkPayloadLength(payloadString) {
*
* @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
@@ -83,17 +83,10 @@ function OlmDevice(sessionStore) {
this._sessionStore = sessionStore;
this._pickleKey = "DEFAULT_KEY";
let e2eKeys;
const account = new Olm.Account();
try {
_initialise_account(this._sessionStore, this._pickleKey, account);
e2eKeys = JSON.parse(account.identity_keys());
} finally {
account.free();
}
this.deviceCurve25519Key = e2eKeys.curve25519;
this.deviceEd25519Key = e2eKeys.ed25519;
// 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.
@@ -103,15 +96,47 @@ function OlmDevice(sessionStore) {
// 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.
// When we decrypt a message and the message index matches a previously
// decrypted message, one possible cause of that is that we are decrypting
// the same event, and may not indicate an actual replay attack. For
// example, this could happen if we receive events, forget about them, and
// then re-fetch them when we backfill. So we store the event ID and
// timestamp corresponding to each message index when we first decrypt it,
// and compare these against the event ID and timestamp every time we use
// that same index. If they match, then we're probably decrypting the same
// event and we don't consider it a replay attack.
//
// Keys are strings of form "<senderKey>|<session_id>|<message_index>"
// Values are true.
// Values are objects of the form "{id: <event id>, timestamp: <ts>}"
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) {
@@ -222,9 +247,9 @@ OlmDevice.prototype._getUtility = function(func) {
* Signs a message with the ed25519 key for this account.
*
* @param {string} message message to be signed
* @return {string} base64-encoded signature
* @return {Promise<string>} base64-encoded signature
*/
OlmDevice.prototype.sign = function(message) {
OlmDevice.prototype.sign = async function(message) {
return this._getAccount(function(account) {
return account.sign(message);
});
@@ -237,7 +262,7 @@ OlmDevice.prototype.sign = function(message) {
* <tt>curve25519</tt>, which is itself an object mapping key id to Curve25519
* key.
*/
OlmDevice.prototype.getOneTimeKeys = function() {
OlmDevice.prototype.getOneTimeKeys = async function() {
return this._getAccount(function(account) {
return JSON.parse(account.one_time_keys());
});
@@ -250,15 +275,13 @@ OlmDevice.prototype.getOneTimeKeys = function() {
* @return {number} number of keys
*/
OlmDevice.prototype.maxNumberOfOneTimeKeys = function() {
return this._getAccount(function(account) {
return account.max_number_of_one_time_keys();
});
return this._maxOneTimeKeys;
};
/**
* Marks all of the one-time keys as published.
*/
OlmDevice.prototype.markKeysAsPublished = function() {
OlmDevice.prototype.markKeysAsPublished = async function() {
const self = this;
this._getAccount(function(account) {
account.mark_keys_as_published();
@@ -271,7 +294,7 @@ OlmDevice.prototype.markKeysAsPublished = function() {
*
* @param {number} numKeys number of keys to generate
*/
OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
OlmDevice.prototype.generateOneTimeKeys = async function(numKeys) {
const self = this;
this._getAccount(function(account) {
account.generate_one_time_keys(numKeys);
@@ -288,7 +311,7 @@ OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
* @param {string} theirOneTimeKey remote user's one-time Curve25519 key
* @return {string} sessionId for the outbound session.
*/
OlmDevice.prototype.createOutboundSession = function(
OlmDevice.prototype.createOutboundSession = async function(
theirIdentityKey, theirOneTimeKey,
) {
const self = this;
@@ -318,7 +341,7 @@ OlmDevice.prototype.createOutboundSession = function(
* @raises {Error} if the received message was not valid (for instance, it
* didn't use a valid one-time key).
*/
OlmDevice.prototype.createInboundSession = function(
OlmDevice.prototype.createInboundSession = async function(
theirDeviceIdentityKey, message_type, ciphertext,
) {
if (message_type !== 0) {
@@ -353,9 +376,9 @@ OlmDevice.prototype.createInboundSession = function(
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @return {string[]} a list of known session ids for the device
* @return {Promise<string[]>} a list of known session ids for the device
*/
OlmDevice.prototype.getSessionIdsForDevice = function(theirDeviceIdentityKey) {
OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityKey) {
const sessions = this._sessionStore.getEndToEndSessions(
theirDeviceIdentityKey,
);
@@ -367,10 +390,10 @@ OlmDevice.prototype.getSessionIdsForDevice = function(theirDeviceIdentityKey) {
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @return {string?} session id, or null if no established session
* @return {Promise<?string>} session id, or null if no established session
*/
OlmDevice.prototype.getSessionIdForDevice = function(theirDeviceIdentityKey) {
const sessionIds = this.getSessionIdsForDevice(theirDeviceIdentityKey);
OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKey) {
const sessionIds = await this.getSessionIdsForDevice(theirDeviceIdentityKey);
if (sessionIds.length === 0) {
return null;
}
@@ -390,8 +413,8 @@ OlmDevice.prototype.getSessionIdForDevice = function(theirDeviceIdentityKey) {
* @param {string} deviceIdentityKey Curve25519 identity key for the device
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
*/
OlmDevice.prototype.getSessionInfoForDevice = function(deviceIdentityKey) {
const sessionIds = this.getSessionIdsForDevice(deviceIdentityKey);
OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey) {
const sessionIds = await this.getSessionIdsForDevice(deviceIdentityKey);
sessionIds.sort();
const info = [];
@@ -419,9 +442,9 @@ OlmDevice.prototype.getSessionInfoForDevice = function(deviceIdentityKey) {
* @param {string} sessionId the id of the active session
* @param {string} payloadString payload to be encrypted and sent
*
* @return {string} ciphertext
* @return {Promise<string>} ciphertext
*/
OlmDevice.prototype.encryptMessage = function(
OlmDevice.prototype.encryptMessage = async function(
theirDeviceIdentityKey, sessionId, payloadString,
) {
const self = this;
@@ -444,9 +467,9 @@ OlmDevice.prototype.encryptMessage = function(
* @param {number} message_type message_type field from the received message
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {string} decrypted payload.
* @return {Promise<string>} decrypted payload.
*/
OlmDevice.prototype.decryptMessage = function(
OlmDevice.prototype.decryptMessage = async function(
theirDeviceIdentityKey, sessionId, message_type, ciphertext,
) {
const self = this;
@@ -468,10 +491,10 @@ OlmDevice.prototype.decryptMessage = function(
* @param {number} message_type message_type field from the received message
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {boolean} true if the received message is a prekey message which matches
* @return {Promise<boolean>} true if the received message is a prekey message which matches
* the given session.
*/
OlmDevice.prototype.matchesSession = function(
OlmDevice.prototype.matchesSession = async function(
theirDeviceIdentityKey, sessionId, message_type, ciphertext,
) {
if (message_type !== 0) {
@@ -582,27 +605,30 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
// 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} roomId
* @param {string} senderCurve25519Key
* @param {string} sessionId
* @param {Olm.InboundGroupSession} session
* @param {object} keysClaimed Other keys the sender claims.
* @param {InboundGroupSessionData} sessionData
* @private
*/
OlmDevice.prototype._saveInboundGroupSession = function(
roomId, senderCurve25519Key, sessionId, session, keysClaimed,
senderCurve25519Key, sessionId, sessionData,
) {
const r = {
room_id: roomId,
session: session.pickle(this._pickleKey),
keysClaimed: keysClaimed,
};
this._sessionStore.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, JSON.stringify(r),
senderCurve25519Key, sessionId, JSON.stringify(sessionData),
);
};
@@ -612,8 +638,8 @@ OlmDevice.prototype._saveInboundGroupSession = function(
* @param {string} roomId
* @param {string} senderKey
* @param {string} sessionId
* @param {function(Olm.InboundGroupSession, Object<string, string>): T} func
* function to call. Second argument is the map of keys claimed by the session.
* @param {function(Olm.InboundGroupSession, InboundGroupSessionData): T} func
* function to call.
*
* @return {null} the sessionId is unknown
*
@@ -647,7 +673,7 @@ OlmDevice.prototype._getInboundGroupSession = function(
const session = new Olm.InboundGroupSession();
try {
session.unpickle(this._pickleKey, r.session);
return func(session, r.keysClaimed || {});
return func(session, r);
} finally {
session.free();
}
@@ -658,17 +684,23 @@ OlmDevice.prototype._getInboundGroupSession = function(
*
* @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 = function(
roomId, senderKey, sessionId, sessionKey, keysClaimed,
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) {
function updateSession(session, sessionData) {
console.log("Update for megolm session " + senderKey + "/" + sessionId);
// for now we just ignore updates. TODO: implement something here
@@ -686,14 +718,26 @@ OlmDevice.prototype.addInboundGroupSession = function(
// new session.
const session = new Olm.InboundGroupSession();
try {
session.create(sessionKey);
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(
roomId, senderKey, sessionId, session, keysClaimed,
senderKey, sessionId, sessionData,
);
} finally {
session.free();
@@ -706,9 +750,9 @@ OlmDevice.prototype.addInboundGroupSession = function(
*
* @param {module:crypto/OlmDevice.MegolmSessionData} data session data
*/
OlmDevice.prototype.importInboundGroupSession = function(data) {
OlmDevice.prototype.importInboundGroupSession = async function(data) {
/* if we already have this session, consider updating it */
function updateSession(session) {
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
@@ -733,9 +777,16 @@ OlmDevice.prototype.importInboundGroupSession = function(data) {
"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.room_id, data.sender_key, data.session_id, session,
data.sender_claimed_keys,
data.sender_key, data.session_id, sessionData,
);
} finally {
session.free();
@@ -749,18 +800,21 @@ OlmDevice.prototype.importInboundGroupSession = function(data) {
* @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
* @param {string} eventId ID of the event being decrypted
* @param {Number} timestamp timestamp of the event being decrypted
*
* @return {null} the sessionId is unknown
*
* @return {{result: string, keysProved: Object<string, string>, keysClaimed:
* Object<string, string>}} result
* @return {Promise<{result: string, senderKey: string,
* forwardingCurve25519KeyChain: Array<string>,
* keysClaimed: Object<string, string>}>}
*/
OlmDevice.prototype.decryptGroupMessage = function(
roomId, senderKey, sessionId, body,
OlmDevice.prototype.decryptGroupMessage = async function(
roomId, senderKey, sessionId, body, eventId, timestamp,
) {
const self = this;
function decrypt(session, keysClaimed) {
function decrypt(session, sessionData) {
const res = session.decrypt(body);
let plaintext = res.plaintext;
@@ -769,27 +823,34 @@ OlmDevice.prototype.decryptGroupMessage = function(
plaintext = res;
} else {
// Check if we have seen this message index before to detect replay attacks.
// If the event ID and timestamp are specified, and the match the event ID
// and timestamp from the last time we used this message index, then we
// don't consider it a replay attack.
const messageIndexKey = senderKey + "|" + sessionId + "|" + res.message_index;
if (messageIndexKey in self._inboundGroupSessionMessageIndexes) {
throw new Error(
"Duplicate message index, possible replay attack: " +
messageIndexKey,
);
const msgInfo = self._inboundGroupSessionMessageIndexes[messageIndexKey];
if (msgInfo.id !== eventId || msgInfo.timestamp !== timestamp) {
throw new Error(
"Duplicate message index, possible replay attack: " +
messageIndexKey,
);
}
}
self._inboundGroupSessionMessageIndexes[messageIndexKey] = true;
self._inboundGroupSessionMessageIndexes[messageIndexKey] = {
id: eventId,
timestamp: timestamp,
};
}
// the sender must have had the senderKey to persuade us to save the
// session.
const keysProved = {curve25519: senderKey};
sessionData.session = session.pickle(self._pickleKey);
self._saveInboundGroupSession(
roomId, senderKey, sessionId, session, keysClaimed,
senderKey, sessionId, sessionData,
);
return {
result: plaintext,
keysClaimed: keysClaimed,
keysProved: keysProved,
keysClaimed: sessionData.keysClaimed || {},
senderKey: senderKey,
forwardingCurve25519KeyChain: sessionData.forwardingCurve25519KeyChain || [],
};
}
@@ -798,14 +859,82 @@ OlmDevice.prototype.decryptGroupMessage = function(
);
};
/**
* 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 {module:crypto/OlmDevice.MegolmSessionData} exported session data
* @return {Promise<module:crypto/OlmDevice.MegolmSessionData>} exported session data
*/
OlmDevice.prototype.exportInboundGroupSession = function(senderKey, sessionId) {
OlmDevice.prototype.exportInboundGroupSession = async function(senderKey, sessionId) {
const s = this._sessionStore.getEndToEndInboundGroupSession(
senderKey, sessionId,
);
@@ -828,6 +957,8 @@ OlmDevice.prototype.exportInboundGroupSession = function(senderKey, sessionId) {
"room_id": r.room_id,
"session_id": sessionId,
"session_key": session.export_session(messageIndex),
"forwarding_curve25519_key_chain":
session.forwardingCurve25519KeyChain || [],
};
} finally {
session.free();
+363
View File
@@ -0,0 +1,363 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import utils from '../utils';
/**
* Internal module. Management of outgoing room key requests.
*
* See https://docs.google.com/document/d/1m4gQkcnJkxNuBmb5NoFCIadIY-DyqqNAS3lloE73BlQ
* for draft documentation on what we're supposed to be implementing here.
*
* @module
*/
// delay between deciding we want some keys, and sending out the request, to
// allow for (a) it turning up anyway, (b) grouping requests together
const SEND_KEY_REQUESTS_DELAY_MS = 500;
/** possible states for a room key request
*
* The state machine looks like:
*
* |
* V (cancellation requested)
* UNSENT -----------------------------+
* | |
* | (send successful) |
* V |
* SENT |
* | |
* | (cancellation requested) |
* V |
* CANCELLATION_PENDING |
* | |
* | (cancellation sent) |
* V |
* (deleted) <---------------------------+
*
* @enum {number}
*/
const ROOM_KEY_REQUEST_STATES = {
/** request not yet sent */
UNSENT: 0,
/** request sent, awaiting reply */
SENT: 1,
/** reply received, cancellation not yet sent */
CANCELLATION_PENDING: 2,
};
export default class OutgoingRoomKeyRequestManager {
constructor(baseApis, deviceId, cryptoStore) {
this._baseApis = baseApis;
this._deviceId = deviceId;
this._cryptoStore = cryptoStore;
// handle for the delayed call to _sendOutgoingRoomKeyRequests. Non-null
// if the callback has been set, or if it is still running.
this._sendOutgoingRoomKeyRequestsTimer = null;
// sanity check to ensure that we don't end up with two concurrent runs
// of _sendOutgoingRoomKeyRequests
this._sendOutgoingRoomKeyRequestsRunning = false;
this._clientRunning = false;
}
/**
* Called when the client is started. Sets background processes running.
*/
start() {
this._clientRunning = true;
// set the timer going, to handle any requests which didn't get sent
// on the previous run of the client.
this._startTimer();
}
/**
* Called when the client is stopped. Stops any running background processes.
*/
stop() {
console.log('stopping OutgoingRoomKeyRequestManager');
// stop the timer on the next run
this._clientRunning = false;
}
/**
* Send off a room key request, if we haven't already done so.
*
* The `requestBody` is compared (with a deep-equality check) against
* previous queued or sent requests and if it matches, no change is made.
* Otherwise, a request is added to the pending list, and a job is started
* in the background to send it.
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* @param {Array<{userId: string, deviceId: string}>} recipients
*
* @returns {Promise} resolves when the request has been added to the
* pending list (or we have established that a similar request already
* exists)
*/
sendRoomKeyRequest(requestBody, recipients) {
return this._cryptoStore.getOrAddOutgoingRoomKeyRequest({
requestBody: requestBody,
recipients: recipients,
requestId: this._baseApis.makeTxnId(),
state: ROOM_KEY_REQUEST_STATES.UNSENT,
}).then((req) => {
if (req.state === ROOM_KEY_REQUEST_STATES.UNSENT) {
this._startTimer();
}
});
}
/**
* Cancel room key requests, if any match the given details
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
*
* @returns {Promise} resolves when the request has been updated in our
* pending list.
*/
cancelRoomKeyRequest(requestBody) {
return this._cryptoStore.getOutgoingRoomKeyRequest(
requestBody,
).then((req) => {
if (!req) {
// no request was made for this key
return;
}
switch (req.state) {
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING:
// nothing to do here
return;
case ROOM_KEY_REQUEST_STATES.UNSENT:
// just delete it
// FIXME: ghahah we may have attempted to send it, and
// not yet got a successful response. So the server
// may have seen it, so we still need to send a cancellation
// in that case :/
console.log(
'deleting unnecessary room key request for ' +
stringifyRequestBody(requestBody),
);
return this._cryptoStore.deleteOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT,
);
case ROOM_KEY_REQUEST_STATES.SENT:
// send a cancellation.
return this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.SENT, {
state: ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
cancellationTxnId: this._baseApis.makeTxnId(),
},
).then((updatedReq) => {
if (!updatedReq) {
// updateOutgoingRoomKeyRequest couldn't find the
// request in state ROOM_KEY_REQUEST_STATES.SENT,
// so we must have raced with another tab to mark
// the request cancelled. There is no point in
// sending another cancellation since the other tab
// will do it.
console.log(
'Tried to cancel room key request for ' +
stringifyRequestBody(requestBody) +
' but it was already cancelled in another tab',
);
return;
}
// We don't want to wait for the timer, so we send it
// immediately. (We might actually end up racing with the timer,
// but that's ok: even if we make the request twice, we'll do it
// with the same transaction_id, so only one message will get
// sent).
//
// (We also don't want to wait for the response from the server
// here, as it will slow down processing of received keys if we
// do.)
this._sendOutgoingRoomKeyRequestCancellation(
updatedReq,
).catch((e) => {
console.error(
"Error sending room key request cancellation;"
+ " will retry later.", e,
);
this._startTimer();
}).done();
});
default:
throw new Error('unhandled state: ' + req.state);
}
});
}
// start the background timer to send queued requests, if the timer isn't
// already running
_startTimer() {
if (this._sendOutgoingRoomKeyRequestsTimer) {
return;
}
const startSendingOutgoingRoomKeyRequests = () => {
if (this._sendOutgoingRoomKeyRequestsRunning) {
throw new Error("RoomKeyRequestSend already in progress!");
}
this._sendOutgoingRoomKeyRequestsRunning = true;
this._sendOutgoingRoomKeyRequests().finally(() => {
this._sendOutgoingRoomKeyRequestsRunning = false;
}).catch((e) => {
// this should only happen if there is an indexeddb error,
// in which case we're a bit stuffed anyway.
console.warn(
`error in OutgoingRoomKeyRequestManager: ${e}`,
);
}).done();
};
this._sendOutgoingRoomKeyRequestsTimer = global.setTimeout(
startSendingOutgoingRoomKeyRequests,
SEND_KEY_REQUESTS_DELAY_MS,
);
}
// look for and send any queued requests. Runs itself recursively until
// there are no more requests, or there is an error (in which case, the
// timer will be restarted before the promise resolves).
_sendOutgoingRoomKeyRequests() {
if (!this._clientRunning) {
this._sendOutgoingRoomKeyRequestsTimer = null;
return Promise.resolve();
}
console.log("Looking for queued outgoing room key requests");
return this._cryptoStore.getOutgoingRoomKeyRequestByState([
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
ROOM_KEY_REQUEST_STATES.UNSENT,
]).then((req) => {
if (!req) {
console.log("No more outgoing room key requests");
this._sendOutgoingRoomKeyRequestsTimer = null;
return;
}
let prom;
if (req.state === ROOM_KEY_REQUEST_STATES.UNSENT) {
prom = this._sendOutgoingRoomKeyRequest(req);
} else { // must be a cancellation
prom = this._sendOutgoingRoomKeyRequestCancellation(req);
}
return prom.then(() => {
// go around the loop again
return this._sendOutgoingRoomKeyRequests();
}).catch((e) => {
console.error("Error sending room key request; will retry later.", e);
this._sendOutgoingRoomKeyRequestsTimer = null;
this._startTimer();
}).done();
});
}
// given a RoomKeyRequest, send it and update the request record
_sendOutgoingRoomKeyRequest(req) {
console.log(
`Requesting keys for ${stringifyRequestBody(req.requestBody)}` +
` from ${stringifyRecipientList(req.recipients)}` +
`(id ${req.requestId})`,
);
const requestMessage = {
action: "request",
requesting_device_id: this._deviceId,
request_id: req.requestId,
body: req.requestBody,
};
return this._sendMessageToDevices(
requestMessage, req.recipients, req.requestId,
).then(() => {
return this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT,
{ state: ROOM_KEY_REQUEST_STATES.SENT },
);
});
}
// given a RoomKeyRequest, cancel it and delete the request record
_sendOutgoingRoomKeyRequestCancellation(req) {
console.log(
`Sending cancellation for key request for ` +
`${stringifyRequestBody(req.requestBody)} to ` +
`${stringifyRecipientList(req.recipients)} ` +
`(cancellation id ${req.cancellationTxnId})`,
);
const requestMessage = {
action: "request_cancellation",
requesting_device_id: this._deviceId,
request_id: req.requestId,
};
return this._sendMessageToDevices(
requestMessage, req.recipients, req.cancellationTxnId,
).then(() => {
return this._cryptoStore.deleteOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
);
});
}
// send a RoomKeyRequest to a list of recipients
_sendMessageToDevices(message, recipients, txnId) {
const contentMap = {};
for (const recip of recipients) {
if (!contentMap[recip.userId]) {
contentMap[recip.userId] = {};
}
contentMap[recip.userId][recip.deviceId] = message;
}
return this._baseApis.sendToDevice(
'm.room_key_request', contentMap, txnId,
);
}
}
function stringifyRequestBody(requestBody) {
// we assume that the request is for megolm keys, which are identified by
// room id and session id
return requestBody.room_id + " / " + requestBody.session_id;
}
function stringifyRecipientList(recipients) {
return '['
+ utils.map(recipients, (r) => `${r.userId}:${r.deviceId}`).join(",")
+ ']';
}
+134 -115
View File
@@ -13,14 +13,14 @@ 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
* @module
*/
const utils = require("../../utils");
import Promise from 'bluebird';
/**
* map of registered encryption algorithm classes. A map from string to {@link
@@ -28,7 +28,7 @@ const utils = require("../../utils");
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
*/
module.exports.ENCRYPTION_CLASSES = {};
export const ENCRYPTION_CLASSES = {};
/**
* map of registered encryption algorithm classes. Map from string to {@link
@@ -36,12 +36,11 @@ module.exports.ENCRYPTION_CLASSES = {};
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
*/
module.exports.DECRYPTION_CLASSES = {};
export const DECRYPTION_CLASSES = {};
/**
* base type for encryption implementations
*
* @constructor
* @alias module:crypto/algorithms/base.EncryptionAlgorithm
*
* @param {object} params parameters
@@ -53,103 +52,122 @@ module.exports.DECRYPTION_CLASSES = {};
* @param {string} params.roomId The ID of the room we will be sending to
* @param {object} params.config The body of the m.room.encryption event
*/
const EncryptionAlgorithm = function(params) {
this._userId = params.userId;
this._deviceId = params.deviceId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._baseApis = params.baseApis;
this._roomId = params.roomId;
};
/** */
module.exports.EncryptionAlgorithm = EncryptionAlgorithm;
class EncryptionAlgorithm {
constructor(params) {
this._userId = params.userId;
this._deviceId = params.deviceId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._baseApis = params.baseApis;
this._roomId = params.roomId;
}
/**
* 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
*/
/**
* Encrypt a message event
*
* @method module:crypto/algorithms/base.EncryptionAlgorithm.encryptMessage
* @abstract
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
/**
* Called when the membership of a member of the room changes.
*
* @param {module:models/event.MatrixEvent} event event causing the change
* @param {module:models/room-member} member user whose membership changed
* @param {string=} oldMembership previous membership
*/
EncryptionAlgorithm.prototype.onRoomMembership = function(
event, member, oldMembership,
) {};
/**
* Called when the membership of a member of the room changes.
*
* @param {module:models/event.MatrixEvent} event event causing the change
* @param {module:models/room-member} member user whose membership changed
* @param {string=} oldMembership previous membership
* @public
*/
onRoomMembership(event, member, oldMembership) {
}
}
export {EncryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
/**
* base type for decryption implementations
*
* @constructor
* @alias module:crypto/algorithms/base.DecryptionAlgorithm
*
* @param {object} params parameters
* @param {string} params.userId The UserID for the local user
* @param {module:crypto} params.crypto crypto core
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
* @param {string=} params.roomId The ID of the room we will be receiving
* from. Null for to-device events.
*/
const DecryptionAlgorithm = function(params) {
this._userId = params.userId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._roomId = params.roomId;
};
/** */
module.exports.DecryptionAlgorithm = DecryptionAlgorithm;
class DecryptionAlgorithm {
constructor(params) {
this._userId = params.userId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._baseApis = params.baseApis;
this._roomId = params.roomId;
}
/**
* Decrypt an event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent
* @abstract
*
* @param {object} event raw event
*
* @return {null} if the event referred to an unknown megolm session
* @return {module:crypto.DecryptionResult} decryption result
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
/**
* Decrypt an event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent
* @abstract
*
* @param {MatrixEvent} event undecrypted event
*
* @return {Promise<module:crypto~EventDecryptionResult>} promise which
* resolves once we have finished decrypting. Rejects with an
* `algorithms.DecryptionError` if there is a problem decrypting the event.
*/
/**
* Handle a key event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent
*
* @param {module:models/event.MatrixEvent} params event key event
*/
DecryptionAlgorithm.prototype.onRoomKeyEvent = function(params) {
// ignore by default
};
/**
* Handle a key event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent
*
* @param {module:models/event.MatrixEvent} params event key event
*/
onRoomKeyEvent(params) {
// ignore by default
}
/**
* Import a room key
*
* @param {module:crypto/OlmDevice.MegolmSessionData} session
*/
DecryptionAlgorithm.prototype.importRoomKey = function(session) {
// ignore by default
};
/**
* Import a room key
*
* @param {module:crypto/OlmDevice.MegolmSessionData} session
*/
importRoomKey(session) {
// ignore by default
}
/**
* Determine if we have the keys necessary to respond to a room key request
*
* @param {module:crypto~IncomingRoomKeyRequest} keyRequest
* @return {Promise<boolean>} true if we have the keys and could (theoretically) share
* them; else false.
*/
hasKeysForKeyRequest(keyRequest) {
return Promise.resolve(false);
}
/**
* Send the response to a room key request
*
* @param {module:crypto~IncomingRoomKeyRequest} keyRequest
*/
shareKeysWithDevice(keyRequest) {
throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm");
}
}
export {DecryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
/**
* Exception thrown when decryption fails
*
* @alias module:crypto/algorithms/base.DecryptionError
* @constructor
* @param {string} msg user-visible message describing the problem
*
* @param {Object=} details key/value pairs reported in the logs but not shown
@@ -157,50 +175,51 @@ DecryptionAlgorithm.prototype.importRoomKey = function(session) {
*
* @extends Error
*/
const DecryptionError = function(msg, details) {
this.name = 'DecryptionError';
this.message = msg;
this.details = details;
};
utils.inherits(DecryptionError, Error);
/** override the string used when logging
*
* @returns {String}
*/
DecryptionError.prototype.toString = function() {
let result = this.name + '[msg: ' + this.message;
if (this.details) {
result += ', ' +
Object.keys(this.details).map(
(k) => k + ': ' + this.details[k],
).join(', ');
class DecryptionError extends Error {
constructor(msg, details) {
super(msg);
this.name = 'DecryptionError';
this.details = details;
}
result += ']';
/**
* override the string used when logging
*
* @returns {String}
*/
toString() {
let result = this.name + '[msg: ' + this.message;
return result;
};
if (this.details) {
result += ', ' +
Object.keys(this.details).map(
(k) => k + ': ' + this.details[k],
).join(', ');
}
module.exports.DecryptionError = DecryptionError;
result += ']';
return result;
}
}
export {DecryptionError}; // https://github.com/jsdoc3/jsdoc/issues/1272
/**
* Exception thrown specifically when we want to warn the user to consider
* the security of their conversation before continuing
*
* @constructor
* @param {string} msg message describing the problem
* @param {Object} devices userId -> {deviceId -> object}
* set of unknown devices per user we're warning about
* @extends Error
*/
module.exports.UnknownDeviceError = function(msg, devices) {
this.name = "UnknownDeviceError";
this.message = msg;
this.devices = devices;
};
utils.inherits(module.exports.UnknownDeviceError, Error);
export class UnknownDeviceError extends Error {
constructor(msg, devices) {
super(msg);
this.name = "UnknownDeviceError";
this.devices = devices;
}
}
/**
* Registers an encryption/decryption class for a particular algorithm
@@ -215,7 +234,7 @@ utils.inherits(module.exports.UnknownDeviceError, Error);
* 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;
};
export function registerAlgorithm(algorithm, encryptor, decryptor) {
ENCRYPTION_CLASSES[algorithm] = encryptor;
DECRYPTION_CLASSES[algorithm] = decryptor;
}
+413 -128
View File
@@ -21,7 +21,7 @@ limitations under the License.
* @module crypto/algorithms/megolm
*/
const q = require("q");
import Promise from 'bluebird';
const utils = require("../../utils");
const olmlib = require("../olmlib");
@@ -74,6 +74,14 @@ OutboundSessionInfo.prototype.needsRotation = function(
return false;
};
OutboundSessionInfo.prototype.markSharedWithDevice = function(
userId, deviceId, chainIndex,
) {
if (!this.sharedWithDevices[userId]) {
this.sharedWithDevices[userId] = {};
}
this.sharedWithDevices[userId][deviceId] = chainIndex;
};
/**
* Determine if this session has been shared with devices which it shouldn't
@@ -132,7 +140,7 @@ function MegolmEncryption(params) {
// are using, and which devices we have shared the keys with. It resolves
// with an OutboundSessionInfo (or undefined, for the first message in the
// room).
this._setupPromise = q();
this._setupPromise = Promise.resolve();
// default rotation periods
this._sessionRotationPeriodMsgs = 100;
@@ -166,7 +174,7 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
// Updates `session` to hold the final OutboundSessionInfo.
//
// returns a promise which resolves once the keyshare is successful.
function prepareSession(oldSession) {
async function prepareSession(oldSession) {
session = oldSession;
// need to make a brand new session?
@@ -183,7 +191,8 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
}
if (!session) {
session = self._prepareNewSession();
console.log(`Starting new megolm session for room ${self._roomId}`);
session = await self._prepareNewSession();
}
// now check if we need to share with any devices
@@ -244,16 +253,157 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
*
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*/
MegolmEncryption.prototype._prepareNewSession = function() {
const session_id = this._olmDevice.createOutboundGroupSession();
const key = this._olmDevice.getOutboundGroupSessionKey(session_id);
MegolmEncryption.prototype._prepareNewSession = async function() {
const sessionId = this._olmDevice.createOutboundGroupSession();
const key = this._olmDevice.getOutboundGroupSessionKey(sessionId);
this._olmDevice.addInboundGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, session_id,
await this._olmDevice.addInboundGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId,
key.key, {ed25519: this._olmDevice.deviceEd25519Key},
);
return new OutboundSessionInfo(session_id);
return new OutboundSessionInfo(sessionId);
};
/**
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {number} chainIndex current chain index
*
* @param {object<userId, deviceId>} devicemap
* mapping from userId to deviceId to {@link module:crypto~OlmSessionResult}
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @return {array<object<userid, deviceInfo>>}
*/
MegolmEncryption.prototype._splitUserDeviceMap = function(
session, chainIndex, devicemap, devicesByUser,
) {
const maxToDeviceMessagesPerRequest = 20;
// use an array where the slices of a content map gets stored
const mapSlices = [];
let currentSliceId = 0; // start inserting in the first slice
let entriesInCurrentSlice = 0;
for (const userId of Object.keys(devicesByUser)) {
const devicesToShareWith = devicesByUser[userId];
const sessionResults = devicemap[userId];
for (let i = 0; i < devicesToShareWith.length; i++) {
const deviceInfo = devicesToShareWith[i];
const deviceId = deviceInfo.deviceId;
const sessionResult = sessionResults[deviceId];
if (!sessionResult.sessionId) {
// no session with this device, probably because there
// were no one-time keys.
//
// we could send them a to_device message anyway, as a
// signal that they have missed out on the key sharing
// message because of the lack of keys, but there's not
// much point in that really; it will mostly serve to clog
// up to_device inboxes.
// mark this device as "handled" because we don't want to try
// to claim a one-time-key for dead devices on every message.
session.markSharedWithDevice(userId, deviceId, chainIndex);
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
continue;
}
console.log(
"share keys with device " + userId + ":" + deviceId,
);
if (entriesInCurrentSlice > maxToDeviceMessagesPerRequest) {
// the current slice is filled up. Start inserting into the next slice
entriesInCurrentSlice = 0;
currentSliceId++;
}
if (!mapSlices[currentSliceId]) {
mapSlices[currentSliceId] = [];
}
mapSlices[currentSliceId].push({
userId: userId,
deviceInfo: deviceInfo,
});
entriesInCurrentSlice++;
}
}
return mapSlices;
};
/**
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {number} chainIndex current chain index
*
* @param {object<userId, deviceInfo>} userDeviceMap
* mapping from userId to deviceInfo
*
* @param {object} payload fields to include in the encrypted payload
*
* @return {module:client.Promise} Promise which resolves once the key sharing
* for the given userDeviceMap is generated and has been sent.
*/
MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
session, chainIndex, userDeviceMap, payload,
) {
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
const contentMap = {};
const promises = [];
for (let i = 0; i < userDeviceMap.length; i++) {
const val = userDeviceMap[i];
const userId = val.userId;
const deviceInfo = val.deviceInfo;
const deviceId = deviceInfo.deviceId;
if (!contentMap[userId]) {
contentMap[userId] = {};
}
contentMap[userId][deviceId] = encryptedContent;
promises.push(
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this._userId,
this._deviceId,
this._olmDevice,
userId,
deviceInfo,
payload,
),
);
}
return Promise.all(promises).then(() => {
return this._baseApis.sendToDevice("m.room.encrypted", contentMap).then(() => {
// store that we successfully uploaded the keys of the current slice
for (const userId of Object.keys(contentMap)) {
for (const deviceId of Object.keys(contentMap[userId])) {
session.markSharedWithDevice(
userId, deviceId, chainIndex,
);
}
}
});
});
};
/**
@@ -263,13 +413,8 @@ MegolmEncryption.prototype._prepareNewSession = function() {
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @return {module:client.Promise} Promise which resolves once the key sharing
* message has been sent.
*/
MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUser) {
const self = this;
MegolmEncryption.prototype._shareKeyWithDevices = async function(session, devicesByUser) {
const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
const payload = {
type: "m.room_key",
@@ -282,98 +427,28 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUse
},
};
const contentMap = {};
return olmlib.ensureOlmSessionsForDevices(
const devicemap = await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser,
).then(function(devicemap) {
let haveTargets = false;
);
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
const userDeviceMaps = this._splitUserDeviceMap(
session, key.chain_index, devicemap, devicesByUser,
);
const devicesToShareWith = devicesByUser[userId];
const sessionResults = devicemap[userId];
for (let i = 0; i < userDeviceMaps.length; i++) {
try {
await this._encryptAndSendKeysToDevices(
session, key.chain_index, userDeviceMaps[i], payload,
);
console.log(`Completed megolm keyshare in ${this._roomId} `
+ `(slice ${i + 1}/${userDeviceMaps.length})`);
} catch (e) {
console.log(`megolm keyshare in ${this._roomId} `
+ `(slice ${i + 1}/${userDeviceMaps.length}) failed`);
for (let i = 0; i < devicesToShareWith.length; i++) {
const deviceInfo = devicesToShareWith[i];
const deviceId = deviceInfo.deviceId;
const sessionResult = sessionResults[deviceId];
if (!sessionResult.sessionId) {
// no session with this device, probably because there
// were no one-time keys.
//
// we could send them a to_device message anyway, as a
// signal that they have missed out on the key sharing
// message because of the lack of keys, but there's not
// much point in that really; it will mostly serve to clog
// up to_device inboxes.
//
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
continue;
}
console.log(
"sharing keys with device " + userId + ":" + deviceId,
);
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
self._userId,
self._deviceId,
self._olmDevice,
userId,
deviceInfo,
payload,
);
if (!contentMap[userId]) {
contentMap[userId] = {};
}
contentMap[userId][deviceId] = encryptedContent;
haveTargets = true;
}
throw e;
}
if (!haveTargets) {
return q();
}
// TODO: retries
return self._baseApis.sendToDevice("m.room.encrypted", contentMap);
}).then(function() {
// Add the devices we have shared with to session.sharedWithDevices.
//
// we deliberately iterate over devicesByUser (ie, the devices we
// attempted to share with) rather than the contentMap (those we did
// share with), because we don't want to try to claim a one-time-key
// for dead devices on every message.
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
if (!session.sharedWithDevices[userId]) {
session.sharedWithDevices[userId] = {};
}
const devicesToShareWith = devicesByUser[userId];
for (let i = 0; i < devicesToShareWith.length; i++) {
const deviceInfo = devicesToShareWith[i];
session.sharedWithDevices[userId][deviceInfo.deviceId] =
key.chain_index;
}
}
});
}
};
/**
@@ -387,6 +462,8 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUse
*/
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
const self = this;
console.log(`Starting to encrypt event for ${this._roomId}`);
return this._getDevicesInRoom(room).then(function(devicesInRoom) {
// check if any of these devices are not yet known to the user.
// if so, warn the user so they can verify or ignore.
@@ -513,8 +590,11 @@ function MegolmDecryption(params) {
base.DecryptionAlgorithm.call(this, params);
// events which we couldn't decrypt due to unknown sessions / indexes: map from
// senderKey|sessionId to list of MatrixEvents
// senderKey|sessionId to Set of MatrixEvents
this._pendingEvents = {};
// this gets stubbed out by the unit tests.
this.olmlib = olmlib;
}
utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
@@ -523,10 +603,12 @@ utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
*
* @param {MatrixEvent} event
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
* returns a promise which resolves to a
* {@link module:crypto~EventDecryptionResult} once we have finished
* decrypting, or rejects with an `algorithms.DecryptionError` if there is a
* problem decrypting the event.
*/
MegolmDecryption.prototype.decryptEvent = function(event) {
MegolmDecryption.prototype.decryptEvent = async function(event) {
const content = event.getWireContent();
if (!content.sender_key || !content.session_id ||
@@ -535,14 +617,22 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
throw new base.DecryptionError("Missing fields in input");
}
// we add the event to the pending list *before* we start decryption.
//
// then, if the key turns up while decryption is in progress (and
// decryption fails), we will schedule a retry.
// (fixes https://github.com/vector-im/riot-web/issues/5001)
this._addEventToPendingList(event);
let res;
try {
res = this._olmDevice.decryptGroupMessage(
res = await this._olmDevice.decryptGroupMessage(
event.getRoomId(), content.sender_key, content.session_id, content.ciphertext,
event.getId(), event.getTs(),
);
} catch (e) {
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
this._addEventToPendingList(event);
this._requestKeysForEvent(event);
}
throw new base.DecryptionError(
e.toString(), {
@@ -553,7 +643,13 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
if (res === null) {
// We've got a message for a session we don't have.
this._addEventToPendingList(event);
//
// (XXX: We might actually have received this key since we started
// decrypting, in which case we'll have scheduled a retry, and this
// request will be redundant. We could probably check to see if the
// event is still in the pending list; if not, a retry will have been
// scheduled, so we needn't send out the request here.)
this._requestKeysForEvent(event);
throw new base.DecryptionError(
"The sender's device has not sent us the keys for this message.",
{
@@ -562,6 +658,10 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
);
}
// success. We can remove the event from the pending list, if that hasn't
// already happened.
this._removeEventFromPendingList(event);
const payload = JSON.parse(res.result);
// belt-and-braces check that the room id matches that indicated by the HS
@@ -573,13 +673,39 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
);
}
event.setClearData(payload, res.keysProved, res.keysClaimed);
return {
clearEvent: payload,
senderCurve25519Key: res.senderKey,
claimedEd25519Key: res.keysClaimed.ed25519,
forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain,
};
};
MegolmDecryption.prototype._requestKeysForEvent = function(event) {
const sender = event.getSender();
const wireContent = event.getWireContent();
// send the request to all of our own devices, and the
// original sending device if it wasn't us.
const recipients = [{
userId: this._userId, deviceId: '*',
}];
if (sender != this._userId) {
recipients.push({
userId: sender, deviceId: wireContent.device_id,
});
}
this._crypto.requestRoomKey({
room_id: event.getRoomId(),
algorithm: wireContent.algorithm,
sender_key: wireContent.sender_key,
session_id: wireContent.session_id,
}, recipients);
};
/**
* Add an event to the list of those we couldn't decrypt the first time we
* saw them.
* Add an event to the list of those awaiting their session keys.
*
* @private
*
@@ -589,11 +715,32 @@ MegolmDecryption.prototype._addEventToPendingList = function(event) {
const content = event.getWireContent();
const k = content.sender_key + "|" + content.session_id;
if (!this._pendingEvents[k]) {
this._pendingEvents[k] = [];
this._pendingEvents[k] = new Set();
}
this._pendingEvents[k].push(event);
this._pendingEvents[k].add(event);
};
/**
* Remove an event from the list of those awaiting their session keys.
*
* @private
*
* @param {module:models/event.MatrixEvent} event
*/
MegolmDecryption.prototype._removeEventFromPendingList = function(event) {
const content = event.getWireContent();
const k = content.sender_key + "|" + content.session_id;
if (!this._pendingEvents[k]) {
return;
}
this._pendingEvents[k].delete(event);
if (this._pendingEvents[k].size === 0) {
delete this._pendingEvents[k];
}
};
/**
* @inheritdoc
*
@@ -601,8 +748,11 @@ MegolmDecryption.prototype._addEventToPendingList = function(event) {
*/
MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
const content = event.getContent();
const senderKey = event.getSenderKey();
const sessionId = content.session_id;
let senderKey = event.getSenderKey();
let forwardingKeyChain = [];
let exportFormat = false;
let keysClaimed;
if (!content.room_id ||
!sessionId ||
@@ -611,21 +761,161 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
console.error("key event is missing fields");
return;
}
if (!senderKey) {
console.error("key event has no sender key (not encrypted?)");
return;
}
if (event.getType() == "m.forwarded_room_key") {
exportFormat = true;
forwardingKeyChain = content.forwarding_curve25519_key_chain;
if (!utils.isArray(forwardingKeyChain)) {
forwardingKeyChain = [];
}
// copy content before we modify it
forwardingKeyChain = forwardingKeyChain.slice();
forwardingKeyChain.push(senderKey);
senderKey = content.sender_key;
if (!senderKey) {
console.error("forwarded_room_key event is missing sender_key field");
return;
}
const ed25519Key = content.sender_claimed_ed25519_key;
if (!ed25519Key) {
console.error(
`forwarded_room_key_event is missing sender_claimed_ed25519_key field`,
);
return;
}
keysClaimed = {
ed25519: ed25519Key,
};
} else {
keysClaimed = event.getKeysClaimed();
}
console.log(`Adding key for megolm session ${senderKey}|${sessionId}`);
this._olmDevice.addInboundGroupSession(
content.room_id, senderKey, sessionId,
content.session_key, event.getKeysClaimed(),
);
content.room_id, senderKey, forwardingKeyChain, sessionId,
content.session_key, keysClaimed,
exportFormat,
).then(() => {
// cancel any outstanding room key requests for this session
this._crypto.cancelRoomKeyRequest({
algorithm: content.algorithm,
room_id: content.room_id,
session_id: content.session_id,
sender_key: senderKey,
});
// have another go at decrypting events sent with this session.
this._retryDecryption(senderKey, sessionId);
// have another go at decrypting events sent with this session.
this._retryDecryption(senderKey, sessionId);
}).catch((e) => {
console.error(`Error handling m.room_key_event: ${e}`);
});
};
/**
* @inheritdoc
*/
MegolmDecryption.prototype.hasKeysForKeyRequest = function(keyRequest) {
const body = keyRequest.requestBody;
return this._olmDevice.hasInboundSessionKeys(
body.room_id,
body.sender_key,
body.session_id,
// TODO: ratchet index
);
};
/**
* @inheritdoc
*/
MegolmDecryption.prototype.shareKeysWithDevice = function(keyRequest) {
const userId = keyRequest.userId;
const deviceId = keyRequest.deviceId;
const deviceInfo = this._crypto.getStoredDevice(userId, deviceId);
const body = keyRequest.requestBody;
this.olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, {
[userId]: [deviceInfo],
},
).then((devicemap) => {
const olmSessionResult = devicemap[userId][deviceId];
if (!olmSessionResult.sessionId) {
// no session with this device, probably because there
// were no one-time keys.
//
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
return null;
}
console.log(
"sharing keys for session " + body.sender_key + "|"
+ body.session_id + " with device "
+ userId + ":" + deviceId,
);
return this._buildKeyForwardingMessage(
body.room_id, body.sender_key, body.session_id,
);
}).then((payload) => {
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
return this.olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this._userId,
this._deviceId,
this._olmDevice,
userId,
deviceInfo,
payload,
).then(() => {
const contentMap = {
[userId]: {
[deviceId]: encryptedContent,
},
};
// TODO: retries
return this._baseApis.sendToDevice("m.room.encrypted", contentMap);
});
}).done();
};
MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
roomId, senderKey, sessionId,
) {
const key = await this._olmDevice.getInboundGroupSessionKey(
roomId, senderKey, sessionId,
);
return {
type: "m.forwarded_room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: roomId,
sender_key: senderKey,
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
session_id: sessionId,
session_key: key.key,
chain_index: key.chain_index,
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain,
},
};
};
/**
* @inheritdoc
@@ -655,13 +945,8 @@ MegolmDecryption.prototype._retryDecryption = function(senderKey, sessionId) {
delete this._pendingEvents[k];
for (let i = 0; i < pending.length; i++) {
try {
this.decryptEvent(pending[i]);
console.log("successful re-decryption of", pending[i]);
} catch (e) {
console.log("Still can't decrypt", pending[i], e.stack || e);
}
for (const ev of pending) {
ev.attemptDecryption(this._crypto);
}
};
+33 -18
View File
@@ -20,7 +20,7 @@ limitations under the License.
*
* @module crypto/algorithms/olm
*/
const q = require('q');
import Promise from 'bluebird';
const utils = require("../../utils");
const olmlib = require("../olmlib");
@@ -60,7 +60,7 @@ OlmEncryption.prototype._ensureSession = function(roomMembers) {
if (this._sessionPrepared) {
// prep already done
return q();
return Promise.resolve();
}
const self = this;
@@ -107,6 +107,8 @@ OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
ciphertext: {},
};
const promises = [];
for (let i = 0; i < users.length; ++i) {
const userId = users[i];
const devices = self._crypto.getStoredDevicesForUser(userId);
@@ -123,15 +125,17 @@ OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
continue;
}
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
self._userId, self._deviceId, self._olmDevice,
userId, deviceInfo, payloadFields,
promises.push(
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
self._userId, self._deviceId, self._olmDevice,
userId, deviceInfo, payloadFields,
),
);
}
}
return encryptedContent;
return Promise.all(promises).return(encryptedContent);
});
};
@@ -153,10 +157,12 @@ utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
*
* @param {MatrixEvent} event
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
* returns a promise which resolves to a
* {@link module:crypto~EventDecryptionResult} once we have finished
* decrypting. Rejects with an `algorithms.DecryptionError` if there is a
* problem decrypting the event.
*/
OlmDecryption.prototype.decryptEvent = function(event) {
OlmDecryption.prototype.decryptEvent = async function(event) {
const content = event.getWireContent();
const deviceKey = content.sender_key;
const ciphertext = content.ciphertext;
@@ -172,7 +178,7 @@ OlmDecryption.prototype.decryptEvent = function(event) {
let payloadString;
try {
payloadString = this._decryptMessage(deviceKey, message);
payloadString = await this._decryptMessage(deviceKey, message);
} catch (e) {
throw new base.DecryptionError(
"Bad Encrypted Message", {
@@ -222,9 +228,14 @@ OlmDecryption.prototype.decryptEvent = function(event) {
);
}
event.setClearData(payload, {curve25519: deviceKey}, payload.keys || {});
};
const claimedKeys = payload.keys || {};
return {
clearEvent: payload,
senderCurve25519Key: deviceKey,
claimedEd25519Key: claimedKeys.ed25519 || null,
};
};
/**
* Attempt to decrypt an Olm message
@@ -234,15 +245,19 @@ OlmDecryption.prototype.decryptEvent = function(event) {
*
* @return {string} payload, if decrypted successfully.
*/
OlmDecryption.prototype._decryptMessage = function(theirDeviceIdentityKey, message) {
const sessionIds = this._olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
OlmDecryption.prototype._decryptMessage = async function(
theirDeviceIdentityKey, message,
) {
const sessionIds = await this._olmDevice.getSessionIdsForDevice(
theirDeviceIdentityKey,
);
// try each session in turn.
const decryptionErrors = {};
for (let i = 0; i < sessionIds.length; i++) {
const sessionId = sessionIds[i];
try {
const payload = this._olmDevice.decryptMessage(
const payload = await this._olmDevice.decryptMessage(
theirDeviceIdentityKey, sessionId, message.type, message.body,
);
console.log(
@@ -251,7 +266,7 @@ OlmDecryption.prototype._decryptMessage = function(theirDeviceIdentityKey, messa
);
return payload;
} catch (e) {
const foundSession = this._olmDevice.matchesSession(
const foundSession = await this._olmDevice.matchesSession(
theirDeviceIdentityKey, sessionId, message.type, message.body,
);
@@ -289,7 +304,7 @@ OlmDecryption.prototype._decryptMessage = function(theirDeviceIdentityKey, messa
let res;
try {
res = this._olmDevice.createInboundSession(
res = await this._olmDevice.createInboundSession(
theirDeviceIdentityKey, message.type, message.body,
);
} catch (e) {
+534 -322
View File
File diff suppressed because it is too large Load Diff
+58 -47
View File
@@ -20,7 +20,7 @@ limitations under the License.
* Utilities common to olm encryption algorithms
*/
const q = require('q');
import Promise from 'bluebird';
const anotherjson = require('another-json');
const utils = require("../utils");
@@ -48,14 +48,17 @@ module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
* @param {string} recipientUserId
* @param {module:crypto/deviceinfo} recipientDevice
* @param {object} payloadFields fields to include in the encrypted payload
*
* Returns a promise which resolves (to undefined) when the payload
* has been encrypted into `resultsObject`
*/
module.exports.encryptMessageForDevice = function(
module.exports.encryptMessageForDevice = async function(
resultsObject,
ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice,
payloadFields,
) {
const deviceKey = recipientDevice.getIdentityKey();
const sessionId = olmDevice.getSessionIdForDevice(deviceKey);
const sessionId = await 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.
@@ -99,7 +102,7 @@ module.exports.encryptMessageForDevice = function(
utils.extend(payload, payloadFields);
resultsObject[deviceKey] = olmDevice.encryptMessage(
resultsObject[deviceKey] = await olmDevice.encryptMessage(
deviceKey, sessionId, JSON.stringify(payload),
);
};
@@ -118,7 +121,7 @@ module.exports.encryptMessageForDevice = function(
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
*/
module.exports.ensureOlmSessionsForDevices = function(
module.exports.ensureOlmSessionsForDevices = async function(
olmDevice, baseApis, devicesByUser,
) {
const devicesWithoutSession = [
@@ -136,7 +139,7 @@ module.exports.ensureOlmSessionsForDevices = function(
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
const sessionId = olmDevice.getSessionIdForDevice(key);
const sessionId = await olmDevice.getSessionIdForDevice(key);
if (sessionId === null) {
devicesWithoutSession.push([userId, deviceId]);
}
@@ -148,7 +151,7 @@ module.exports.ensureOlmSessionsForDevices = function(
}
if (devicesWithoutSession.length === 0) {
return q(result);
return result;
}
// TODO: this has a race condition - if we try to send another message
@@ -158,55 +161,60 @@ module.exports.ensureOlmSessionsForDevices = function(
// That should eventually resolve itself, but it's poor form.
const oneTimeKeyAlgorithm = "signed_curve25519";
return baseApis.claimOneTimeKeys(
const res = await baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm,
).then(function(res) {
const otk_res = res.one_time_keys || {};
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
);
const otk_res = res.one_time_keys || {};
const promises = [];
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
const userRes = otk_res[userId] || {};
const devices = devicesByUser[userId];
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
if (result[userId][deviceId].sessionId) {
// we already have a result for this device
continue;
}
const userRes = otk_res[userId] || {};
const devices = devicesByUser[userId];
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
if (result[userId][deviceId].sessionId) {
// we already have a result for this device
continue;
}
const deviceRes = userRes[deviceId] || {};
let oneTimeKey = null;
for (const keyId in deviceRes) {
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
oneTimeKey = deviceRes[keyId];
}
const deviceRes = userRes[deviceId] || {};
let oneTimeKey = null;
for (const keyId in deviceRes) {
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
oneTimeKey = deviceRes[keyId];
}
if (!oneTimeKey) {
console.warn(
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
") for device " + userId + ":" + deviceId,
);
continue;
}
const sid = _verifyKeyAndStartSession(
olmDevice, oneTimeKey, userId, deviceInfo,
);
result[userId][deviceId].sessionId = sid;
}
if (!oneTimeKey) {
console.warn(
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
") for device " + userId + ":" + deviceId,
);
continue;
}
promises.push(
_verifyKeyAndStartSession(
olmDevice, oneTimeKey, userId, deviceInfo,
).then((sid) => {
result[userId][deviceId].sessionId = sid;
}),
);
}
return result;
});
}
await Promise.all(promises);
return result;
};
function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
const deviceId = deviceInfo.deviceId;
try {
_verifySignature(
await _verifySignature(
olmDevice, oneTimeKey, userId, deviceId,
deviceInfo.getFingerprint(),
);
@@ -220,7 +228,7 @@ function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
let sid;
try {
sid = olmDevice.createOutboundSession(
sid = await olmDevice.createOutboundSession(
deviceInfo.getIdentityKey(), oneTimeKey.key,
);
} catch (e) {
@@ -249,8 +257,11 @@ function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
* @param {string} signingDeviceId ID of the device whose signature should be checked
*
* @param {string} signingKey base64-ed ed25519 public key
*
* Returns a promise which resolves (to undefined) if the the signature is good,
* or rejects with an Error if it is bad.
*/
const _verifySignature = module.exports.verifySignature = function(
const _verifySignature = module.exports.verifySignature = async function(
olmDevice, obj, signingUserId, signingDeviceId, signingKey,
) {
const signKeyId = "ed25519:" + signingDeviceId;
+34
View File
@@ -0,0 +1,34 @@
/**
* Internal module. Defintions for storage for the crypto module
*
* @module
*/
/**
* Abstraction of things that can store data required for end-to-end encryption
*
* @interface CryptoStore
*/
/**
* Represents an outgoing room key request
*
* @typedef {Object} OutgoingRoomKeyRequest
*
* @property {string} requestId unique id for this request. Used for both
* an id within the request for later pairing with a cancellation, and for
* the transaction id when sending the to_device messages to our local
* server.
*
* @property {string?} cancellationTxnId
* transaction id for the cancellation, if any
*
* @property {Array<{userId: string, deviceId: string}>} recipients
* list of recipients for the request
*
* @property {module:crypto~RoomKeyRequestBody} requestBody
* parameters for the request.
*
* @property {Number} state current state of this request (states are defined
* in {@link module:crypto/OutgoingRoomKeyRequestManager~ROOM_KEY_REQUEST_STATES})
*/
@@ -0,0 +1,291 @@
import Promise from 'bluebird';
import utils from '../../utils';
export const VERSION = 1;
/**
* Implementation of a CryptoStore which is backed by an existing
* IndexedDB connection. Generally you want IndexedDBCryptoStore
* which connects to the database and defers to one of these.
*
* @implements {module:crypto/store/base~CryptoStore}
*/
export class Backend {
/**
* @param {IDBDatabase} db
*/
constructor(db) {
this._db = db;
// make sure we close the db on `onversionchange` - otherwise
// attempts to delete the database will block (and subsequent
// attempts to re-create it will also block).
db.onversionchange = (ev) => {
console.log(`versionchange for indexeddb ${this._dbName}: closing`);
db.close();
};
}
/**
* Look for an existing outgoing room key request, and if none is found,
* add a new one
*
* @param {module:crypto/store/base~OutgoingRoomKeyRequest} request
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
* same instance as passed in, or the existing one.
*/
getOrAddOutgoingRoomKeyRequest(request) {
const requestBody = request.requestBody;
const deferred = Promise.defer();
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
txn.onerror = deferred.reject;
// first see if we already have an entry for this request.
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
if (existing) {
// this entry matches the request - return it.
console.log(
`already have key request outstanding for ` +
`${requestBody.room_id} / ${requestBody.session_id}: ` +
`not sending another`,
);
deferred.resolve(existing);
return;
}
// we got to the end of the list without finding a match
// - add the new request.
console.log(
`enqueueing key request for ${requestBody.room_id} / ` +
requestBody.session_id,
);
const store = txn.objectStore("outgoingRoomKeyRequests");
store.add(request);
txn.onsuccess = () => { deferred.resolve(request); };
});
return deferred.promise;
}
/**
* Look for an existing room key request
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* existing request to look for
*
* @return {Promise} resolves to the matching
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found
*/
getOutgoingRoomKeyRequest(requestBody) {
const deferred = Promise.defer();
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
txn.onerror = deferred.reject;
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
deferred.resolve(existing);
});
return deferred.promise;
}
/**
* look for an existing room key request in the db
*
* @private
* @param {IDBTransaction} txn database transaction
* @param {module:crypto~RoomKeyRequestBody} requestBody
* existing request to look for
* @param {Function} callback function to call with the results of the
* search. Either passed a matching
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found.
*/
_getOutgoingRoomKeyRequest(txn, requestBody, callback) {
const store = txn.objectStore("outgoingRoomKeyRequests");
const idx = store.index("session");
const cursorReq = idx.openCursor([
requestBody.room_id,
requestBody.session_id,
]);
cursorReq.onsuccess = (ev) => {
const cursor = ev.target.result;
if(!cursor) {
// no match found
callback(null);
return;
}
const existing = cursor.value;
if (utils.deepCompare(existing.requestBody, requestBody)) {
// got a match
callback(existing);
return;
}
// look at the next entry in the index
cursor.continue();
};
}
/**
* Look for room key requests by state
*
* @param {Array<Number>} wantedStates list of acceptable states
*
* @return {Promise} resolves to the a
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* there are no pending requests in those states. If there are multiple
* requests in those states, an arbitrary one is chosen.
*/
getOutgoingRoomKeyRequestByState(wantedStates) {
if (wantedStates.length === 0) {
return Promise.resolve(null);
}
// this is a bit tortuous because we need to make sure we do the lookup
// in a single transaction, to avoid having a race with the insertion
// code.
// index into the wantedStates array
let stateIndex = 0;
let result;
function onsuccess(ev) {
const cursor = ev.target.result;
if (cursor) {
// got a match
result = cursor.value;
return;
}
// try the next state in the list
stateIndex++;
if (stateIndex >= wantedStates.length) {
// no matches
return;
}
const wantedState = wantedStates[stateIndex];
const cursorReq = ev.target.source.openCursor(wantedState);
cursorReq.onsuccess = onsuccess;
}
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
const store = txn.objectStore("outgoingRoomKeyRequests");
const wantedState = wantedStates[stateIndex];
const cursorReq = store.index("state").openCursor(wantedState);
cursorReq.onsuccess = onsuccess;
return promiseifyTxn(txn).then(() => result);
}
/**
* Look for an existing room key request by id and state, and update it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
* @param {Object} updates name/value map of updates to apply
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* updated request, or null if no matching row was found
*/
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
let result = null;
function onsuccess(ev) {
const cursor = ev.target.result;
if (!cursor) {
return;
}
const data = cursor.value;
if (data.state != expectedState) {
console.warn(
`Cannot update room key request from ${expectedState} ` +
`as it was already updated to ${data.state}`,
);
return;
}
Object.assign(data, updates);
cursor.update(data);
result = data;
}
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
const cursorReq = txn.objectStore("outgoingRoomKeyRequests")
.openCursor(requestId);
cursorReq.onsuccess = onsuccess;
return promiseifyTxn(txn).then(() => result);
}
/**
* Look for an existing room key request by id and state, and delete it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
*
* @returns {Promise} resolves once the operation is completed
*/
deleteOutgoingRoomKeyRequest(requestId, expectedState) {
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
const cursorReq = txn.objectStore("outgoingRoomKeyRequests")
.openCursor(requestId);
cursorReq.onsuccess = (ev) => {
const cursor = ev.target.result;
if (!cursor) {
return;
}
const data = cursor.value;
if (data.state != expectedState) {
console.warn(
`Cannot delete room key request in state ${data.state} `
+ `(expected ${expectedState})`,
);
return;
}
cursor.delete();
};
return promiseifyTxn(txn);
}
}
export function upgradeDatabase(db, oldVersion) {
console.log(
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
+ ` to ${VERSION}`,
);
if (oldVersion < 1) { // The database did not previously exist.
createDatabase(db);
}
// Expand as needed.
}
function createDatabase(db) {
const outgoingRoomKeyRequestsStore =
db.createObjectStore("outgoingRoomKeyRequests", { keyPath: "requestId" });
// we assume that the RoomKeyRequestBody will have room_id and session_id
// properties, to make the index efficient.
outgoingRoomKeyRequestsStore.createIndex("session",
["requestBody.room_id", "requestBody.session_id"],
);
outgoingRoomKeyRequestsStore.createIndex("state", "state");
}
function promiseifyTxn(txn) {
return new Promise((resolve, reject) => {
txn.oncomplete = resolve;
txn.onerror = reject;
});
}
+229
View File
@@ -0,0 +1,229 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import MemoryCryptoStore from './memory-crypto-store';
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';
/**
* Internal module. indexeddb storage for e2e.
*
* @module
*/
/**
* An implementation of CryptoStore, which is normally backed by an indexeddb,
* but with fallback to MemoryCryptoStore.
*
* @implements {module:crypto/store/base~CryptoStore}
*/
export default class IndexedDBCryptoStore {
/**
* Create a new IndexedDBCryptoStore
*
* @param {IDBFactory} indexedDB global indexedDB instance
* @param {string} dbName name of db to connect to
*/
constructor(indexedDB, dbName) {
this._indexedDB = indexedDB;
this._dbName = dbName;
this._backendPromise = null;
}
/**
* Ensure the database exists and is up-to-date, or fall back to
* an in-memory store.
*
* @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend,
* or a MemoryCryptoStore
*/
_connect() {
if (this._backendPromise) {
return this._backendPromise;
}
this._backendPromise = new Promise((resolve, reject) => {
if (!this._indexedDB) {
reject(new Error('no indexeddb support available'));
return;
}
console.log(`connecting to indexeddb ${this._dbName}`);
const req = this._indexedDB.open(
this._dbName, IndexedDBCryptoStoreBackend.VERSION,
);
req.onupgradeneeded = (ev) => {
const db = ev.target.result;
const oldVersion = ev.oldVersion;
IndexedDBCryptoStoreBackend.upgradeDatabase(db, oldVersion);
};
req.onblocked = () => {
console.log(
`can't yet open IndexedDBCryptoStore because it is open elsewhere`,
);
};
req.onerror = (ev) => {
reject(ev.target.error);
};
req.onsuccess = (r) => {
const db = r.target.result;
console.log(`connected to indexeddb ${this._dbName}`);
resolve(new IndexedDBCryptoStoreBackend.Backend(db));
};
}).catch((e) => {
// Don't fall back to memory in this case: we'd end up recreating
// a new Olm account in memory and advertising new keys for the
// same device.
if (e.name == 'VersionError') {
throw e;
}
console.warn(
`unable to connect to indexeddb ${this._dbName}` +
`: falling back to in-memory store: ${e}`,
);
return new MemoryCryptoStore();
});
return this._backendPromise;
}
/**
* Delete all data from this store.
*
* @returns {Promise} resolves when the store has been cleared.
*/
deleteAllData() {
return new Promise((resolve, reject) => {
if (!this._indexedDB) {
reject(new Error('no indexeddb support available'));
return;
}
console.log(`Removing indexeddb instance: ${this._dbName}`);
const req = this._indexedDB.deleteDatabase(this._dbName);
req.onblocked = () => {
console.log(
`can't yet delete IndexedDBCryptoStore because it is open elsewhere`,
);
};
req.onerror = (ev) => {
reject(ev.target.error);
};
req.onsuccess = () => {
console.log(`Removed indexeddb instance: ${this._dbName}`);
resolve();
};
}).catch((e) => {
// in firefox, with indexedDB disabled, this fails with a
// DOMError. We treat this as non-fatal, so that people can
// still use the app.
console.warn(`unable to delete IndexedDBCryptoStore: ${e}`);
});
}
/**
* Look for an existing outgoing room key request, and if none is found,
* add a new one
*
* @param {module:crypto/store/base~OutgoingRoomKeyRequest} request
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
* same instance as passed in, or the existing one.
*/
getOrAddOutgoingRoomKeyRequest(request) {
return this._connect().then((backend) => {
return backend.getOrAddOutgoingRoomKeyRequest(request);
});
}
/**
* Look for an existing room key request
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* existing request to look for
*
* @return {Promise} resolves to the matching
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found
*/
getOutgoingRoomKeyRequest(requestBody) {
return this._connect().then((backend) => {
return backend.getOutgoingRoomKeyRequest(requestBody);
});
}
/**
* Look for room key requests by state
*
* @param {Array<Number>} wantedStates list of acceptable states
*
* @return {Promise} resolves to the a
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* there are no pending requests in those states. If there are multiple
* requests in those states, an arbitrary one is chosen.
*/
getOutgoingRoomKeyRequestByState(wantedStates) {
return this._connect().then((backend) => {
return backend.getOutgoingRoomKeyRequestByState(wantedStates);
});
}
/**
* Look for an existing room key request by id and state, and update it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
* @param {Object} updates name/value map of updates to apply
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* updated request, or null if no matching row was found
*/
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
return this._connect().then((backend) => {
return backend.updateOutgoingRoomKeyRequest(
requestId, expectedState, updates,
);
});
}
/**
* Look for an existing room key request by id and state, and delete it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
*
* @returns {Promise} resolves once the operation is completed
*/
deleteOutgoingRoomKeyRequest(requestId, expectedState) {
return this._connect().then((backend) => {
return backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
});
}
}
+199
View File
@@ -0,0 +1,199 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import utils from '../../utils';
/**
* Internal module. in-memory storage for e2e.
*
* @module
*/
/**
* @implements {module:crypto/store/base~CryptoStore}
*/
export default class MemoryCryptoStore {
constructor() {
this._outgoingRoomKeyRequests = [];
}
/**
* Delete all data from this store.
*
* @returns {Promise} Promise which resolves when the store has been cleared.
*/
deleteAllData() {
return Promise.resolve();
}
/**
* Look for an existing outgoing room key request, and if none is found,
* add a new one
*
* @param {module:crypto/store/base~OutgoingRoomKeyRequest} request
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}: either the
* same instance as passed in, or the existing one.
*/
getOrAddOutgoingRoomKeyRequest(request) {
const requestBody = request.requestBody;
return Promise.try(() => {
// first see if we already have an entry for this request.
const existing = this._getOutgoingRoomKeyRequest(requestBody);
if (existing) {
// this entry matches the request - return it.
console.log(
`already have key request outstanding for ` +
`${requestBody.room_id} / ${requestBody.session_id}: ` +
`not sending another`,
);
return existing;
}
// we got to the end of the list without finding a match
// - add the new request.
console.log(
`enqueueing key request for ${requestBody.room_id} / ` +
requestBody.session_id,
);
this._outgoingRoomKeyRequests.push(request);
return request;
});
}
/**
* Look for an existing room key request
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* existing request to look for
*
* @return {Promise} resolves to the matching
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* not found
*/
getOutgoingRoomKeyRequest(requestBody) {
return Promise.resolve(this._getOutgoingRoomKeyRequest(requestBody));
}
/**
* Looks for existing room key request, and returns the result synchronously.
*
* @internal
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* existing request to look for
*
* @return {module:crypto/store/base~OutgoingRoomKeyRequest?}
* the matching request, or null if not found
*/
_getOutgoingRoomKeyRequest(requestBody) {
for (const existing of this._outgoingRoomKeyRequests) {
if (utils.deepCompare(existing.requestBody, requestBody)) {
return existing;
}
}
return null;
}
/**
* Look for room key requests by state
*
* @param {Array<Number>} wantedStates list of acceptable states
*
* @return {Promise} resolves to the a
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}, or null if
* there are no pending requests in those states
*/
getOutgoingRoomKeyRequestByState(wantedStates) {
for (const req of this._outgoingRoomKeyRequests) {
for (const state of wantedStates) {
if (req.state === state) {
return Promise.resolve(req);
}
}
}
return Promise.resolve(null);
}
/**
* Look for an existing room key request by id and state, and update it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
* @param {Object} updates name/value map of updates to apply
*
* @returns {Promise} resolves to
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
* updated request, or null if no matching row was found
*/
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
for (const req of this._outgoingRoomKeyRequests) {
if (req.requestId !== requestId) {
continue;
}
if (req.state != expectedState) {
console.warn(
`Cannot update room key request from ${expectedState} ` +
`as it was already updated to ${req.state}`,
);
return Promise.resolve(null);
}
Object.assign(req, updates);
return Promise.resolve(req);
}
return Promise.resolve(null);
}
/**
* Look for an existing room key request by id and state, and delete it if
* found
*
* @param {string} requestId ID of request to update
* @param {number} expectedState state we expect to find the request in
*
* @returns {Promise} resolves once the operation is completed
*/
deleteOutgoingRoomKeyRequest(requestId, expectedState) {
for (let i = 0; i < this._outgoingRoomKeyRequests.length; i++) {
const req = this._outgoingRoomKeyRequests[i];
if (req.requestId !== requestId) {
continue;
}
if (req.state != expectedState) {
console.warn(
`Cannot delete room key request in state ${req.state} `
+ `(expected ${expectedState})`,
);
return Promise.resolve(null);
}
this._outgoingRoomKeyRequests.splice(i, 1);
return Promise.resolve(req);
}
return Promise.resolve(null);
}
}
+147 -31
View File
@@ -18,7 +18,9 @@ limitations under the License.
* This is an internal module. See {@link MatrixHttpApi} for the public class.
* @module http-api
*/
const q = require("q");
import Promise from 'bluebird';
const parseContentType = require('content-type').parse;
const utils = require("./utils");
// we use our own implementation of setTimeout, so that if we get suspended in
@@ -64,7 +66,7 @@ module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
* @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
* @param {boolean} opts.onlyData True to return only the 'data' component of the
* response (e.g. the parsed HTTP body). If false, requests will return an
* object with the properties <tt>code</tt>, <tt>headers</tt> and <tt>data</tt>.
*
@@ -74,12 +76,15 @@ module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
* requests.
* @param {Number=} opts.localTimeoutMs The default maximum amount of time to wait
* before timing out the request. If not specified, there is no timeout.
* @param {boolean} [opts.useAuthorizationHeader = false] Set to true to use
* Authorization header instead of query param to send the access token to the server.
*/
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.useAuthorizationHeader = Boolean(opts.useAuthorizationHeader);
this.uploads = [];
};
@@ -129,6 +134,10 @@ module.exports.MatrixHttpApi.prototype = {
* invoke on success/failure. See the promise return values for more
* information.
*
* @param {Function=} opts.progressHandler Optional. Called when a chunk of
* data has been uploaded, with an object containing the fields `loaded`
* (number of bytes transferred) and `total` (total size, if known).
*
* @return {module:client.Promise} Resolves to response object, as
* determined by this.opts.onlyData, opts.rawResponse, and
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
@@ -214,7 +223,7 @@ module.exports.MatrixHttpApi.prototype = {
}
if (global.XMLHttpRequest) {
const defer = q.defer();
const defer = Promise.defer();
const xhr = new global.XMLHttpRequest();
upload.xhr = xhr;
const cb = requestCallback(defer, opts.callback, this.opts.onlyData);
@@ -255,7 +264,12 @@ module.exports.MatrixHttpApi.prototype = {
upload.loaded = ev.loaded;
upload.total = ev.total;
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
defer.notify(ev);
if (opts.progressHandler) {
opts.progressHandler({
loaded: ev.loaded,
total: ev.total,
});
}
});
let url = this.opts.baseUrl + "/_matrix/media/v1/upload";
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
@@ -338,7 +352,7 @@ module.exports.MatrixHttpApi.prototype = {
opts.form = params;
}
const defer = q.defer();
const defer = Promise.defer();
this.opts.request(
opts,
requestCallback(defer, callback, this.opts.onlyData),
@@ -364,7 +378,8 @@ module.exports.MatrixHttpApi.prototype = {
*
* @param {Object} data The HTTP JSON body.
*
* @param {Object=} opts additional options
* @param {Object|Number=} opts additional options. If a number is specified,
* this is treated as `opts.localTimeoutMs`.
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
@@ -385,16 +400,37 @@ module.exports.MatrixHttpApi.prototype = {
if (!queryParams) {
queryParams = {};
}
if (!queryParams.access_token) {
queryParams.access_token = this.opts.accessToken;
if (this.useAuthorizationHeader) {
if (isFinite(opts)) {
// opts used to be localTimeoutMs
opts = {
localTimeoutMs: opts,
};
}
if (!opts) {
opts = {};
}
if (!opts.headers) {
opts.headers = {};
}
if (!opts.headers.Authorization) {
opts.headers.Authorization = "Bearer " + this.opts.accessToken;
}
if (queryParams.access_token) {
delete queryParams.access_token;
}
} else {
if (!queryParams.access_token) {
queryParams.access_token = this.opts.accessToken;
}
}
const request_promise = this.request(
const requestPromise = this.request(
callback, method, path, queryParams, data, opts,
);
const self = this;
request_promise.catch(function(err) {
requestPromise.catch(function(err) {
if (err.errcode == 'M_UNKNOWN_TOKEN') {
self.event_emitter.emit("Session.logged_out");
}
@@ -402,7 +438,7 @@ module.exports.MatrixHttpApi.prototype = {
// 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;
return requestPromise;
},
/**
@@ -623,9 +659,33 @@ module.exports.MatrixHttpApi.prototype = {
}
}
const headers = utils.extend({}, opts.headers || {});
const json = opts.json === undefined ? true : opts.json;
let bodyParser = opts.bodyParser;
const defer = q.defer();
// we handle the json encoding/decoding here, because request and
// browser-request make a mess of it. Specifically, they attempt to
// json-decode plain-text error responses, which in turn means that the
// actual error gets swallowed by a SyntaxError.
if (json) {
if (data) {
data = JSON.stringify(data);
headers['content-type'] = 'application/json';
}
if (!headers['accept']) {
headers['accept'] = 'application/json';
}
if (bodyParser === undefined) {
bodyParser = function(rawBody) {
return JSON.parse(rawBody);
};
}
}
const defer = Promise.defer();
let timeoutId;
let timedOut = false;
@@ -662,7 +722,7 @@ module.exports.MatrixHttpApi.prototype = {
withCredentials: false,
qs: queryParams,
body: data,
json: json,
json: false,
timeout: localTimeoutMs,
headers: opts.headers || {},
_matrix_opts: this.opts,
@@ -675,13 +735,9 @@ module.exports.MatrixHttpApi.prototype = {
}
}
// if json is falsy, we won't parse any error response, so need
// to do so before turning it into a MatrixError
const parseErrorJson = !json;
const handlerFn = requestCallback(
defer, callback, self.opts.onlyData,
parseErrorJson,
opts.bodyParser,
bodyParser,
);
handlerFn(err, response, body);
},
@@ -718,15 +774,18 @@ module.exports.MatrixHttpApi.prototype = {
* 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.
* HTTP errors are transformed into javascript errors and the deferred is rejected.
*
* If parseErrorJson is true, we will JSON.parse the body if we get a 4xx error.
* If bodyParser is given, it is used to transform the body of the successful
* responses before passing to the defer/callback.
*
* If onlyData is true, the defer/callback is invoked with the body of the
* response, otherwise the result object (with `code` and `data` fields)
*
*/
const requestCallback = function(
defer, userDefinedCallback, onlyData,
parseErrorJson, bodyParser,
bodyParser,
) {
userDefinedCallback = userDefinedCallback || function() {};
@@ -734,19 +793,12 @@ const requestCallback = function(
if (!err) {
try {
if (response.statusCode >= 400) {
if (parseErrorJson) {
// we won't have json-decoded the response.
body = JSON.parse(body);
}
err = new module.exports.MatrixError(body);
err = parseErrorResponse(response, body);
} else if (bodyParser) {
body = bodyParser(body);
}
} catch (e) {
err = e;
}
if (err) {
err.httpStatus = response.statusCode;
err = new Error(`Error parsing server response: ${e}`);
}
}
@@ -756,6 +808,9 @@ const requestCallback = function(
} else {
const res = {
code: response.statusCode,
// XXX: why do we bother with this? it doesn't work for
// XMLHttpRequest, so clearly we don't use it.
headers: response.headers,
data: body,
};
@@ -765,6 +820,67 @@ const requestCallback = function(
};
};
/**
* Attempt to turn an HTTP error response into a Javascript Error.
*
* If it is a JSON response, we will parse it into a MatrixError. Otherwise
* we return a generic Error.
*
* @param {XMLHttpRequest|http.IncomingMessage} response response object
* @param {String} body raw body of the response
* @returns {Error}
*/
function parseErrorResponse(response, body) {
const httpStatus = response.statusCode;
const contentType = getResponseContentType(response);
let err;
if (contentType) {
if (contentType.type === 'application/json') {
err = new module.exports.MatrixError(JSON.parse(body));
} else if (contentType.type === 'text/plain') {
err = new Error(`Server returned ${httpStatus} error: ${body}`);
}
}
if (!err) {
err = new Error(`Server returned ${httpStatus} error`);
}
err.httpStatus = httpStatus;
return err;
}
/**
* extract the Content-Type header from the response object, and
* parse it to a `{type, parameters}` object.
*
* returns null if no content-type header could be found.
*
* @param {XMLHttpRequest|http.IncomingMessage} response response object
* @returns {{type: String, parameters: Object}?} parsed content-type header, or null if not found
*/
function getResponseContentType(response) {
let contentType;
if (response.getResponseHeader) {
// XMLHttpRequest provides getResponseHeader
contentType = response.getResponseHeader("Content-Type");
} else if (response.headers) {
// request provides http.IncomingMessage which has a message.headers map
contentType = response.headers['content-type'] || null;
}
if (!contentType) {
return null;
}
try {
return parseContentType(contentType);
} catch(e) {
throw new Error(`Error parsing Content-Type '${contentType}': ${e}`);
}
}
/**
* Construct a Matrix error. This is a JavaScript Error with additional
* information specific to the standard Matrix error response.
+8 -6
View File
@@ -17,7 +17,7 @@ limitations under the License.
"use strict";
/** @module interactive-auth */
const q = require("q");
import Promise from 'bluebird';
const url = require("url");
const utils = require("./utils");
@@ -115,11 +115,11 @@ InteractiveAuth.prototype = {
* no suitable authentication flow can be found
*/
attemptAuth: function() {
this._completionDeferred = q.defer();
this._completionDeferred = Promise.defer();
// wrap in a promise so that if _startNextAuthStage
// throws, it rejects the promise in a consistent way
return q().then(() => {
return Promise.resolve().then(() => {
// if we have no flows, try a request (we'll have
// just a session ID in _data if resuming)
if (!this._data.flows) {
@@ -258,12 +258,12 @@ InteractiveAuth.prototype = {
// hackery to make sure that synchronous exceptions end up in the catch
// handler (without the additional event loop entailed by q.fcall or an
// extra q().then)
// extra Promise.resolve().then)
let prom;
try {
prom = this._requestCallback(auth, background);
} catch (e) {
prom = q.reject(e);
prom = Promise.reject(e);
}
prom = prom.then(
@@ -293,7 +293,9 @@ InteractiveAuth.prototype = {
},
);
if (!background) {
prom = prom.catch(this._completionDeferred.reject);
prom = prom.catch((e) => {
this._completionDeferred.reject(e);
});
} else {
// We ignore all failures here (even non-UI auth related ones)
// since we don't want to suddenly fail if the internet connection
+26
View File
@@ -63,6 +63,11 @@ module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
module.exports.InteractiveAuth = require("./interactive-auth");
module.exports.MemoryCryptoStore =
require("./crypto/store/memory-crypto-store").default;
module.exports.IndexedDBCryptoStore =
require("./crypto/store/indexeddb-crypto-store").default;
/**
* Create a new Matrix Call.
* @function
@@ -124,6 +129,19 @@ module.exports.wrapRequest = function(wrapper) {
};
};
let cryptoStoreFactory = () => new module.exports.MemoryCryptoStore;
/**
* Configure a different factory to be used for creating crypto stores
*
* @param {Function} fac a function which will return a new
* {@link module:crypto.store.base~CryptoStore}.
*/
module.exports.setCryptoStoreFactory = function(fac) {
cryptoStoreFactory = fac;
};
/**
* Construct a Matrix Client. Similar to {@link module:client~MatrixClient}
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
@@ -136,6 +154,13 @@ module.exports.wrapRequest = function(wrapper) {
* {@link module:scheduler~MatrixScheduler}.
* @param {requestFunction} opts.request If not set, defaults to the function
* supplied to {@link request} which defaults to the request module from NPM.
*
* @param {module:crypto.store.base~CryptoStore=} opts.cryptoStore
* crypto store implementation. Calls the factory supplied to
* {@link setCryptoStoreFactory} if unspecified; or if no factory has been
* specified, uses a default implementation (indexeddb in the browser,
* in-memory otherwise).
*
* @return {MatrixClient} A new matrix client.
* @see {@link module:client~MatrixClient} for the full list of options for
* <code>opts</code>.
@@ -151,6 +176,7 @@ module.exports.createClient = function(opts) {
localStorage: global.localStorage,
});
opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler();
opts.cryptoStore = opts.cryptoStore || cryptoStoreFactory();
return new module.exports.MatrixClient(opts);
};
+42 -5
View File
@@ -149,13 +149,24 @@ EventTimelineSet.prototype.replaceEventId = function(oldEventId, newEventId) {
* <p>This is used when /sync returns a 'limited' timeline.
*
* @param {string=} backPaginationToken token for back-paginating the new timeline
* @param {?bool} flush Whether to flush the non-live timelines too.
* @param {string=} forwardPaginationToken token for forward-paginating the old live timeline,
* if absent or null, all timelines are reset.
*
* @fires module:client~MatrixClient#event:"Room.timelineReset"
*/
EventTimelineSet.prototype.resetLiveTimeline = function(backPaginationToken, flush) {
EventTimelineSet.prototype.resetLiveTimeline = function(
backPaginationToken, forwardPaginationToken,
) {
// Each EventTimeline has RoomState objects tracking the state at the start
// and end of that timeline. The copies at the end of the live timeline are
// special because they will have listeners attached to monitor changes to
// the current room state, so we move this RoomState from the end of the
// current live timeline to the end of the new one and, if necessary,
// replace it with a newly created one. We also make a copy for the start
// of the new timeline.
// if timeline support is disabled, forget about the old timelines
const resetAllTimelines = !this._timelineSupport || flush;
const resetAllTimelines = !this._timelineSupport || !forwardPaginationToken;
let newTimeline;
if (resetAllTimelines) {
@@ -166,8 +177,10 @@ EventTimelineSet.prototype.resetLiveTimeline = function(backPaginationToken, flu
newTimeline = this.addTimeline();
}
// initialise the state in the new timeline from our last known state
const evMap = this._liveTimeline.getState(EventTimeline.FORWARDS).events;
const oldTimeline = this._liveTimeline;
// Collect the state events from the old timeline
const evMap = oldTimeline.getState(EventTimeline.FORWARDS).events;
const events = [];
for (const evtype in evMap) {
if (!evMap.hasOwnProperty(evtype)) {
@@ -180,13 +193,37 @@ EventTimelineSet.prototype.resetLiveTimeline = function(backPaginationToken, flu
events.push(evMap[evtype][stateKey]);
}
}
// Use those events to initialise the state of the new live timeline
newTimeline.initialiseState(events);
const freshEndState = newTimeline._endState;
// Now clobber the end state of the new live timeline with that from the
// previous live timeline. It will be identical except that we'll keep
// using the same RoomMember objects for the 'live' set of members with any
// listeners still attached
newTimeline._endState = oldTimeline._endState;
// If we're not resetting all timelines, we need to fix up the old live timeline
if (!resetAllTimelines) {
// Firstly, we just stole the old timeline's end state, so it needs a new one.
// Just swap them around and give it the one we just generated for the
// new live timeline.
oldTimeline._endState = freshEndState;
// Now set the forward pagination token on the old live timeline
// so it can be forward-paginated.
oldTimeline.setPaginationToken(
forwardPaginationToken, EventTimeline.FORWARDS,
);
}
// make sure we set the pagination token before firing timelineReset,
// otherwise clients which start back-paginating will fail, and then get
// stuck without realising that they *can* back-paginate.
newTimeline.setPaginationToken(backPaginationToken, EventTimeline.BACKWARDS);
// Now we can swap the live timeline to the new one.
this._liveTimeline = newTimeline;
this.emit("Room.timelineReset", this.room, this, resetAllTimelines);
};
+266 -41
View File
@@ -21,9 +21,9 @@ limitations under the License.
* @module models/event
*/
const EventEmitter = require("events").EventEmitter;
const utils = require('../utils.js');
import Promise from 'bluebird';
import {EventEmitter} from 'events';
import utils from '../utils.js';
/**
* Enum for event statuses.
@@ -112,8 +112,33 @@ module.exports.MatrixEvent = function MatrixEvent(
new Date(this.event.origin_server_ts) : null;
this._clearEvent = {};
this._keysProved = {};
this._keysClaimed = {};
/* curve25519 key which we believe belongs to the sender of the event. See
* getSenderKey()
*/
this._senderCurve25519Key = null;
/* ed25519 key which the sender of this event (for olm) or the creator of
* the megolm session (for megolm) claims to own. See getClaimedEd25519Key()
*/
this._claimedEd25519Key = null;
/* curve25519 keys of devices involved in telling us about the
* _senderCurve25519Key and _claimedEd25519Key.
* See getForwardingCurve25519KeyChain().
*/
this._forwardingCurve25519KeyChain = [];
/* if we have a process decrypting this event, a Promise which resolves
* when it is finished. Normally null.
*/
this._decryptionPromise = null;
/* flag to indicate if we should retry decrypting this event after the
* first attempt (eg, we have received new data which means that a second
* attempt may succeed)
*/
this._retryDecryption = false;
};
utils.inherits(module.exports.MatrixEvent, EventEmitter);
@@ -261,9 +286,18 @@ utils.extend(module.exports.MatrixEvent.prototype, {
* <tt>"m.room.encrypted"</tt>
*
* @param {object} crypto_content raw 'content' for the encrypted event.
* @param {object} keys The local keys claimed and proved by this event.
*
* @param {string} senderCurve25519Key curve25519 key to record for the
* sender of this event.
* See {@link module:models/event.MatrixEvent#getSenderKey}.
*
* @param {string} claimedEd25519Key claimed ed25519 key to record for the
* sender if this event.
* See {@link module:models/event.MatrixEvent#getClaimedEd25519Key}
*/
makeEncrypted: function(crypto_type, crypto_content, keys) {
makeEncrypted: function(
crypto_type, crypto_content, senderCurve25519Key, claimedEd25519Key,
) {
// keep the plain-text data for 'view source'
this._clearEvent = {
type: this.event.type,
@@ -271,8 +305,166 @@ utils.extend(module.exports.MatrixEvent.prototype, {
};
this.event.type = crypto_type;
this.event.content = crypto_content;
this._keysProved = keys;
this._keysClaimed = keys;
this._senderCurve25519Key = senderCurve25519Key;
this._claimedEd25519Key = claimedEd25519Key;
},
/**
* Check if this event is currently being decrypted.
*
* @return {boolean} True if this event is currently being decrypted, else false.
*/
isBeingDecrypted: function() {
return this._decryptionPromise != null;
},
/**
* Check if this event is an encrypted event which we failed to decrypt
*
* (This implies that we might retry decryption at some point in the future)
*
* @return {boolean} True if this event is an encrypted event which we
* couldn't decrypt.
*/
isDecryptionFailure: function() {
return this._clearEvent && this._clearEvent.content &&
this._clearEvent.content.msgtype === "m.bad.encrypted";
},
/**
* Start the process of trying to decrypt this event.
*
* (This is used within the SDK: it isn't intended for use by applications)
*
* @internal
*
* @param {module:crypto} crypto crypto module
*
* @returns {Promise} promise which resolves (to undefined) when the decryption
* attempt is completed.
*/
attemptDecryption: async function(crypto) {
// start with a couple of sanity checks.
if (!this.isEncrypted()) {
throw new Error("Attempt to decrypt event which isn't encrypted");
}
if (
this._clearEvent && this._clearEvent.content &&
this._clearEvent.content.msgtype !== "m.bad.encrypted"
) {
// we may want to just ignore this? let's start with rejecting it.
throw new Error(
"Attempt to decrypt event which has already been encrypted",
);
}
// if we already have a decryption attempt in progress, then it may
// fail because it was using outdated info. We now have reason to
// succeed where it failed before, but we don't want to have multiple
// attempts going at the same time, so just set a flag that says we have
// new info.
//
if (this._decryptionPromise) {
console.log(
`Event ${this.getId()} already being decrypted; queueing a retry`,
);
this._retryDecryption = true;
return this._decryptionPromise;
}
this._decryptionPromise = this._decryptionLoop(crypto);
return this._decryptionPromise;
},
_decryptionLoop: async function(crypto) {
// make sure that this method never runs completely synchronously.
// (doing so would mean that we would clear _decryptionPromise *before*
// it is set in attemptDecryption - and hence end up with a stuck
// `_decryptionPromise`).
await Promise.resolve();
while (true) {
this._retryDecryption = false;
let res;
try {
if (!crypto) {
res = this._badEncryptedMessage("Encryption not enabled");
} else {
res = await crypto.decryptEvent(this);
}
} catch (e) {
if (e.name !== "DecryptionError") {
// not a decryption error: log the whole exception as an error
// (and don't bother with a retry)
console.error(
`Error decrypting event (id=${this.getId()}): ${e.stack || e}`,
);
this._decryptionPromise = null;
this._retryDecryption = false;
return;
}
// see if we have a retry queued.
//
// NB: make sure to keep this check in the same tick of the
// event loop as `_decryptionPromise = null` below - otherwise we
// risk a race:
//
// * A: we check _retryDecryption here and see that it is
// false
// * B: we get a second call to attemptDecryption, which sees
// that _decryptionPromise is set so sets
// _retryDecryption
// * A: we continue below, clear _decryptionPromise, and
// never do the retry.
//
if (this._retryDecryption) {
// decryption error, but we have a retry queued.
console.log(
`Got error decrypting event (id=${this.getId()}: ` +
`${e.message}), but retrying`,
);
continue;
}
// decryption error, no retries queued. Warn about the error and
// set it to m.bad.encrypted.
console.warn(
`Error decrypting event (id=${this.getId()}): ${e}`,
);
res = this._badEncryptedMessage(e.message);
}
// at this point, we've either successfully decrypted the event, or have given up
// (and set res to a 'badEncryptedMessage'). Either way, we can now set the
// cleartext of the event and raise Event.decrypted.
//
// make sure we clear '_decryptionPromise' before sending the 'Event.decrypted' event,
// otherwise the app will be confused to see `isBeingDecrypted` still set when
// there isn't an `Event.decrypted` on the way.
//
// see also notes on _retryDecryption above.
//
this._decryptionPromise = null;
this._retryDecryption = false;
this._setClearData(res);
return;
}
},
_badEncryptedMessage: function(reason) {
return {
clearEvent: {
type: "m.room.message",
content: {
msgtype: "m.bad.encrypted",
body: "** Unable to decrypt: " + reason + " **",
},
},
};
},
/**
@@ -284,19 +476,17 @@ utils.extend(module.exports.MatrixEvent.prototype, {
*
* @fires module:models/event.MatrixEvent#"Event.decrypted"
*
* @param {Object} clearEvent The plaintext payload for the event
* (typically containing <tt>type</tt> and <tt>content</tt> fields).
*
* @param {Object=} keysProved Keys owned by the sender of this event.
* See {@link module:models/event.MatrixEvent#getKeysProved}.
*
* @param {Object=} keysClaimed Keys the sender of this event claims.
* See {@link module:models/event.MatrixEvent#getKeysClaimed}.
* @param {module:crypto~EventDecryptionResult} decryptionResult
* the decryption result, including the plaintext and some key info
*/
setClearData: function(clearEvent, keysProved, keysClaimed) {
this._clearEvent = clearEvent;
this._keysProved = keysProved || {};
this._keysClaimed = keysClaimed || {};
_setClearData: function(decryptionResult) {
this._clearEvent = decryptionResult.clearEvent;
this._senderCurve25519Key =
decryptionResult.senderCurve25519Key || null;
this._claimedEd25519Key =
decryptionResult.claimedEd25519Key || null;
this._forwardingCurve25519KeyChain =
decryptionResult.forwardingCurve25519KeyChain || [];
this.emit("Event.decrypted", this);
},
@@ -309,37 +499,72 @@ utils.extend(module.exports.MatrixEvent.prototype, {
},
/**
* The curve25519 key that sent this event
* The curve25519 key for the device that we think sent this event
*
* For an Olm-encrypted event, this is inferred directly from the DH
* exchange at the start of the session: the curve25519 key is involved in
* the DH exchange, so only a device which holds the private part of that
* key can establish such a session.
*
* For a megolm-encrypted event, it is inferred from the Olm message which
* established the megolm session
*
* @return {string}
*/
getSenderKey: function() {
return this.getKeysProved().curve25519 || null;
},
/**
* The keys that must have been owned by the sender of this encrypted event.
* <p>
* These don't necessarily have to come from this event itself, but may be
* implied by the cryptographic session.
*
* @return {Object<string, string>}
*/
getKeysProved: function() {
return this._keysProved;
return this._senderCurve25519Key;
},
/**
* The additional keys the sender of this encrypted event claims to possess.
* <p>
* These don't necessarily have to come from this event itself, but may be
* implied by the cryptographic session.
* For example megolm messages don't claim keys directly, but instead
* inherit a claim from the olm message that established the session.
*
* Just a wrapper for #getClaimedEd25519Key (q.v.)
*
* @return {Object<string, string>}
*/
getKeysClaimed: function() {
return this._keysClaimed;
return {
ed25519: this._claimedEd25519Key,
};
},
/**
* Get the ed25519 the sender of this event claims to own.
*
* For Olm messages, this claim is encoded directly in the plaintext of the
* event itself. For megolm messages, it is implied by the m.room_key event
* which established the megolm session.
*
* Until we download the device list of the sender, it's just a claim: the
* device list gives a proof that the owner of the curve25519 key used for
* this event (and returned by #getSenderKey) also owns the ed25519 key by
* signing the public curve25519 key with the ed25519 key.
*
* In general, applications should not use this method directly, but should
* instead use MatrixClient.getEventSenderDeviceInfo.
*
* @return {string}
*/
getClaimedEd25519Key: function() {
return this._claimedEd25519Key;
},
/**
* Get the curve25519 keys of the devices which were involved in telling us
* about the claimedEd25519Key and sender curve25519 key.
*
* Normally this will be empty, but in the case of a forwarded megolm
* session, the sender keys are sent to us by another device (the forwarding
* device), which we need to trust to do this. In that case, the result will
* be a list consisting of one entry.
*
* If the device that sent us the key (A) got it from another device which
* it wasn't prepared to vouch for (B), the result will be [A, B]. And so on.
*
* @return {string[]} base64-encoded curve25519 keys, from oldest to newest.
*/
getForwardingCurve25519KeyChain: function() {
return this._forwardingCurve25519KeyChain;
},
getUnsigned: function() {
+95
View File
@@ -0,0 +1,95 @@
/*
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 models/group
*/
const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
/**
* Construct a new Group.
*
* @param {string} groupId The ID of this group.
*
* @prop {string} groupId The ID of this group.
* @prop {string} name The human-readable display name for this group.
* @prop {string} avatarUrl The mxc URL for this group's avatar.
* @prop {string} myMembership The logged in user's membership of this group
* @prop {Object} inviter Infomation about the user who invited the logged in user
* to the group, if myMembership is 'invite'.
* @prop {string} inviter.userId The user ID of the inviter
*/
function Group(groupId) {
this.groupId = groupId;
this.name = null;
this.avatarUrl = null;
this.myMembership = null;
this.inviter = null;
}
utils.inherits(Group, EventEmitter);
Group.prototype.setProfile = function(name, avatarUrl) {
if (this.name === name && this.avatarUrl === avatarUrl) return;
this.name = name || this.groupId;
this.avatarUrl = avatarUrl;
this.emit("Group.profile", this);
};
Group.prototype.setMyMembership = function(membership) {
if (this.myMembership === membership) return;
this.myMembership = membership;
this.emit("Group.myMembership", this);
};
/**
* Sets the 'inviter' property. This does not emit an event (the inviter
* will only change when the user is revited / reinvited to a room),
* so set this before setting myMembership.
* @param {Object} inviter Infomation about who invited us to the room
*/
Group.prototype.setInviter = function(inviter) {
this.inviter = inviter;
};
module.exports = Group;
/**
* Fires whenever a group's profile information is updated.
* This means the 'name' and 'avatarUrl' properties.
* @event module:client~MatrixClient#"Group.profile"
* @param {Group} group The group whose profile was updated.
* @example
* matrixClient.on("Group.profile", function(group){
* var name = group.name;
* });
*/
/**
* Fires whenever the logged in user's membership status of
* the group is updated.
* @event module:client~MatrixClient#"Group.myMembership"
* @param {Group} group The group in which the user's membership changed
* @example
* matrixClient.on("Group.myMembership", function(group){
* var myMembership = group.myMembership;
* });
*/
+15 -7
View File
@@ -33,7 +33,10 @@ const utils = require("../utils");
* @prop {string} roomId The room ID for this member.
* @prop {string} userId The user ID of this member.
* @prop {boolean} typing True if the room member is currently typing.
* @prop {string} name The human-readable name for this room member.
* @prop {string} name The human-readable name for this room member. This will be
* disambiguated with a suffix of " (@user_id:matrix.org)" if another member shares the
* same displayname.
* @prop {string} rawDisplayName The ambiguous displayname of this room member.
* @prop {Number} powerLevel The power level for this room member.
* @prop {Number} powerLevelNorm The normalised power level (0-100) for this
* room member.
@@ -47,6 +50,7 @@ function RoomMember(roomId, userId) {
this.userId = userId;
this.typing = false;
this.name = userId;
this.rawDisplayName = userId;
this.powerLevel = 0;
this.powerLevelNorm = 0;
this.user = null;
@@ -78,6 +82,7 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) {
const oldName = this.name;
this.name = calculateDisplayName(this, event, roomState);
this.rawDisplayName = event.getDirectionalContent().displayname || this.userId;
if (oldMembership !== this.membership) {
this._updateModifiedTime();
this.emit("RoomMember.membership", event, this, oldMembership);
@@ -99,17 +104,20 @@ RoomMember.prototype.setPowerLevelEvent = function(powerLevelEvent) {
if (powerLevelEvent.getType() !== "m.room.power_levels") {
return;
}
let maxLevel = powerLevelEvent.getContent().users_default || 0;
utils.forEach(utils.values(powerLevelEvent.getContent().users), function(lvl) {
const evContent = powerLevelEvent.getDirectionalContent();
let maxLevel = evContent.users_default || 0;
utils.forEach(utils.values(evContent.users), function(lvl) {
maxLevel = Math.max(maxLevel, lvl);
});
const oldPowerLevel = this.powerLevel;
const oldPowerLevelNorm = this.powerLevelNorm;
if (powerLevelEvent.getContent().users[this.userId] !== undefined) {
this.powerLevel = powerLevelEvent.getContent().users[this.userId];
} else if (powerLevelEvent.getContent().users_default !== undefined) {
this.powerLevel = powerLevelEvent.getContent().users_default;
if (evContent.users && evContent.users[this.userId] !== undefined) {
this.powerLevel = evContent.users[this.userId];
} else if (evContent.users_default !== undefined) {
this.powerLevel = evContent.users_default;
} else {
this.powerLevel = 0;
}
+78 -1
View File
@@ -197,6 +197,16 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
member.setPowerLevelEvent(event);
self.emit("RoomState.members", event, self, member);
});
// Go through the sentinel members and see if any of them would be
// affected by the new power levels. If so, replace the sentinel.
for (const userId of Object.keys(self._sentinels)) {
const oldSentinel = self._sentinels[userId];
const newSentinel = new RoomMember(event.getRoomId(), userId);
newSentinel.setMembershipEvent(oldSentinel.events.member, self);
newSentinel.setPowerLevelEvent(event);
self._sentinels[userId] = newSentinel;
}
}
});
};
@@ -246,6 +256,45 @@ RoomState.prototype.getUserIdsWithDisplayName = function(displayName) {
return this._displayNameToUserIds[displayName] || [];
};
/**
* Returns true if userId is in room, event is not redacted and either sender of
* mxEvent or has power level sufficient to redact events other than their own.
* @param {MatrixEvent} mxEvent The event to test permission for
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given used ID can redact given event
*/
RoomState.prototype.maySendRedactionForEvent = function(mxEvent, userId) {
const member = this.getMember(userId);
if (!member || member.membership === 'leave') return false;
if (mxEvent.status || mxEvent.isRedacted()) return false;
if (mxEvent.getSender() === userId) return true;
return this._hasSufficientPowerLevelFor('redact', member.powerLevel);
};
/**
* Returns true if the given power level is sufficient for action
* @param {string} action The type of power level to check
* @param {number} powerLevel The power level of the member
* @return {boolean} true if the given power level is sufficient
*/
RoomState.prototype._hasSufficientPowerLevelFor = function(action, powerLevel) {
const powerLevelsEvent = this.getStateEvents('m.room.power_levels', '');
let powerLevels = {};
if (powerLevelsEvent) {
powerLevels = powerLevelsEvent.getContent();
}
let requiredLevel = 50;
if (powerLevels[action] !== undefined) {
requiredLevel = powerLevels[action];
}
return powerLevel >= requiredLevel;
};
/**
* Short-form for maySendEvent('m.room.message', userId)
* @param {string} userId The user ID of the user to test permission for
@@ -269,7 +318,6 @@ RoomState.prototype.maySendEvent = function(eventType, userId) {
return this._maySendEventOfType(eventType, userId, false);
};
/**
* Returns true if the given MatrixClient has permission to send a state
* event of type `stateEventType` into this room.
@@ -345,6 +393,35 @@ RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
return member.powerLevel >= required_level;
};
/**
* Returns true if the given user ID has permission to trigger notification
* of type `notifLevelKey`
* @param {string} notifLevelKey The level of notification to test (eg. 'room')
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID has permission to trigger a
* notification of this type.
*/
RoomState.prototype.mayTriggerNotifOfType = function(notifLevelKey, userId) {
const member = this.getMember(userId);
if (!member) {
return false;
}
const powerLevelsEvent = this.getStateEvents('m.room.power_levels', '');
let notifLevel = 50;
if (
powerLevelsEvent &&
powerLevelsEvent.getContent() &&
powerLevelsEvent.getContent().notifications &&
powerLevelsEvent.getContent().notifications[notifLevelKey]
) {
notifLevel = powerLevelsEvent.getContent().notifications[notifLevelKey];
}
return member.powerLevel >= notifLevel;
};
/**
* The RoomState class.
*/
+17 -27
View File
@@ -27,6 +27,7 @@ const ContentRepo = require("../content-repo");
const EventTimeline = require("./event-timeline");
const EventTimelineSet = require("./event-timeline-set");
import ReEmitter from '../ReEmitter';
function synthesizeReceipt(userId, event, receiptType) {
// console.log("synthesizing receipt for "+event.getId());
@@ -105,6 +106,8 @@ function Room(roomId, opts) {
opts = opts || {};
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
this.reEmitter = new ReEmitter(this);
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
throw new Error(
"opts.pendingEventOrdering MUST be either 'chronological' or " +
@@ -152,7 +155,7 @@ function Room(roomId, opts) {
// all our per-room timeline sets. the first one is the unfiltered ones;
// the subsequent ones are the filtered ones in no particular order.
this._timelineSets = [new EventTimelineSet(this, opts)];
reEmit(this, this.getUnfilteredTimelineSet(),
this.reEmitter.reEmit(this.getUnfilteredTimelineSet(),
["Room.timeline", "Room.timelineReset"]);
this._fixUpLegacyTimelineFields();
@@ -204,12 +207,16 @@ Room.prototype.getLiveTimeline = function() {
* <p>This is used when /sync returns a 'limited' timeline.
*
* @param {string=} backPaginationToken token for back-paginating the new timeline
* @param {boolean=} flush True to remove all events in all timelines. If false, only
* the live timeline is reset.
* @param {string=} forwardPaginationToken token for forward-paginating the old live timeline,
* if absent or null, all timelines are reset, removing old ones (including the previous live
* timeline which would otherwise be unable to paginate forwards without this token).
* Removing just the old live timeline whilst preserving previous ones is not supported.
*/
Room.prototype.resetLiveTimeline = function(backPaginationToken, flush) {
Room.prototype.resetLiveTimeline = function(backPaginationToken, forwardPaginationToken) {
for (let i = 0; i < this._timelineSets.length; i++) {
this._timelineSets[i].resetLiveTimeline(backPaginationToken, flush);
this._timelineSets[i].resetLiveTimeline(
backPaginationToken, forwardPaginationToken,
);
}
this._fixUpLegacyTimelineFields();
@@ -485,7 +492,7 @@ Room.prototype.getOrCreateFilteredTimelineSet = function(filter) {
}
const opts = Object.assign({ filter: filter }, this._opts);
const timelineSet = new EventTimelineSet(this, opts);
reEmit(this, timelineSet, ["Room.timeline", "Room.timelineReset"]);
this.reEmitter.reEmit(timelineSet, ["Room.timeline", "Room.timelineReset"]);
this._filteredTimelineSets[filter.filterId] = timelineSet;
this._timelineSets.push(timelineSet);
@@ -751,6 +758,8 @@ ALLOWED_TRANSITIONS[EventStatus.CANCELLED] =
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
*/
Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
console.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()}`);
// if the message was sent, we expect an event id
if (newStatus == EventStatus.SENT && !newEventId) {
throw new Error("updatePendingEvent called with status=SENT, " +
@@ -1196,13 +1205,13 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
if (myMemberEvent && myMemberEvent.content.membership == "invite") {
if (room.currentState.getMember(myMemberEvent.sender)) {
// extract who invited us to the room
return "Invite from " + room.currentState.getMember(
return room.currentState.getMember(
myMemberEvent.sender,
).name;
} else if (allMembers[0].events.member) {
// use the sender field from the invite event, although this only
// gets us the mxid
return "Invite from " + myMemberEvent.sender;
return myMemberEvent.sender;
} else {
return "Room Invite";
}
@@ -1252,25 +1261,6 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
}
}
// FIXME: copypasted from sync.js
function reEmit(reEmitEntity, emittableEntity, eventNames) {
utils.forEach(eventNames, function(eventName) {
// setup a listener on the entity (the Room, User, etc) for this event
emittableEntity.on(eventName, function() {
// take the args from the listener and reuse them, adding the
// event name to the arg list so it works with .emit()
// Transformation Example:
// listener on "foo" => function(a,b) { ... }
// Re-emit on "thing" => thing.emit("foo", a, b)
const newArgs = [eventName];
for (let i = 0; i < arguments.length; i++) {
newArgs.push(arguments[i]);
}
reEmitEntity.emit(...newArgs);
});
});
}
/**
* The Room class.
*/
+64 -19
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket 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.
@@ -17,6 +18,8 @@ limitations under the License.
* @module pushprocessor
*/
const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'];
/**
* Construct a Push Processor.
* @constructor
@@ -27,12 +30,11 @@ function PushProcessor(client) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
const matchingRuleFromKindSet = function(ev, kindset, device) {
const rulekinds_in_order = ['override', 'content', 'room', 'sender', 'underride'];
const matchingRuleFromKindSet = (ev, kindset, device) => {
for (let ruleKindIndex = 0;
ruleKindIndex < rulekinds_in_order.length;
ruleKindIndex < RULEKINDS_IN_ORDER.length;
++ruleKindIndex) {
const kind = rulekinds_in_order[ruleKindIndex];
const kind = RULEKINDS_IN_ORDER[ruleKindIndex];
const ruleset = kindset[kind];
for (let ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) {
@@ -46,7 +48,7 @@ function PushProcessor(client) {
continue;
}
if (ruleMatchesEvent(rawrule, ev)) {
if (this.ruleMatchesEvent(rawrule, ev)) {
rule.kind = kind;
return rule;
}
@@ -106,27 +108,38 @@ function PushProcessor(client) {
return rawrule;
};
const ruleMatchesEvent = function(rule, ev) {
let ret = true;
for (let i = 0; i < rule.conditions.length; ++i) {
const cond = rule.conditions[i];
ret &= eventFulfillsCondition(cond, ev);
}
//console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match"));
return ret;
};
const eventFulfillsCondition = function(cond, ev) {
const condition_functions = {
"event_match": eventFulfillsEventMatchCondition,
"device": eventFulfillsDeviceCondition,
"contains_display_name": eventFulfillsDisplayNameCondition,
"room_member_count": eventFulfillsRoomMemberCountCondition,
"sender_notification_permission": eventFulfillsSenderNotifPermCondition,
};
if (condition_functions[cond.kind]) {
return condition_functions[cond.kind](cond, ev);
}
return true;
// unknown conditions: we previously matched all unknown conditions,
// but given that rules can be added to the base rules on a server,
// it's probably better to not match unknown conditions.
return false;
};
const eventFulfillsSenderNotifPermCondition = function(cond, ev) {
const notifLevelKey = cond['key'];
if (!notifLevelKey) {
return false;
}
const room = client.getRoom(ev.getRoomId());
if (!room || !room.currentState) {
return false;
}
// Note that this should not be the current state of the room but the state at
// the point the event is in the DAG. Unfortunately the js-sdk does not store
// this.
return room.currentState.mayTriggerNotifOfType(notifLevelKey, ev.getSender());
};
const eventFulfillsRoomMemberCountCondition = function(cond, ev) {
@@ -215,9 +228,9 @@ function PushProcessor(client) {
// Because micromatch is about 130KB with dependencies,
// and minimatch is not much better.
let pat = escapeRegExp(glob);
pat = pat.replace(/\\\*/, '.*');
pat = pat.replace(/\?/, '.');
pat = pat.replace(/\\\[(!|)(.*)\\]/, function(match, p1, p2, offset, string) {
pat = pat.replace(/\\\*/g, '.*');
pat = pat.replace(/\?/g, '.');
pat = pat.replace(/\\\[(!|)(.*)\\]/g, function(match, p1, p2, offset, string) {
const first = p1 && '^' || '';
const second = p2.replace(/\\\-/, '-');
return '[' + first + second + ']';
@@ -291,6 +304,17 @@ function PushProcessor(client) {
return actionObj;
};
this.ruleMatchesEvent = function(rule, ev) {
let ret = true;
for (let i = 0; i < rule.conditions.length; ++i) {
const cond = rule.conditions[i];
ret &= eventFulfillsCondition(cond, ev);
}
//console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match"));
return ret;
};
/**
* Get the user's push actions for the given event
*
@@ -301,6 +325,27 @@ function PushProcessor(client) {
this.actionsForEvent = function(ev) {
return pushActionsForEventAndRulesets(ev, client.pushRules);
};
/**
* Get one of the users push rules by its ID
*
* @param {string} ruleId The ID of the rule to search for
* @return {object} The push rule, or null if no such rule was found
*/
this.getPushRuleById = function(ruleId) {
for (const scope of ['device', 'global']) {
if (client.pushRules[scope] === undefined) continue;
for (const kind of RULEKINDS_IN_ORDER) {
if (client.pushRules[scope][kind] === undefined) continue;
for (const rule of client.pushRules[scope][kind]) {
if (rule.rule_id === ruleId) return rule;
}
}
}
return null;
};
}
/**
+4 -2
View File
@@ -20,7 +20,7 @@ limitations under the License.
* @module scheduler
*/
const utils = require("./utils");
const q = require("q");
import Promise from 'bluebird';
const DEBUG = false; // set true to enable console logging.
@@ -83,6 +83,8 @@ MatrixScheduler.prototype.removeEventFromQueue = function(event) {
let removed = false;
utils.removeElement(this._queues[name], function(element) {
if (element.event.getId() === event.getId()) {
// XXX we should probably reject the promise?
// https://github.com/matrix-org/matrix-js-sdk/issues/496
removed = true;
return true;
}
@@ -118,7 +120,7 @@ MatrixScheduler.prototype.queueEvent = function(event) {
if (!this._queues[queueName]) {
this._queues[queueName] = [];
}
const defer = q.defer();
const defer = Promise.defer();
this._queues[queueName].push({
event: event,
defer: defer,
+87 -21
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import q from "q";
import Promise from 'bluebird';
import SyncAccumulator from "../sync-accumulator";
import utils from "../utils";
@@ -44,7 +44,7 @@ function createDatabase(db) {
*/
function selectQuery(store, keyRange, resultMapper) {
const query = store.openCursor(keyRange);
return q.Promise((resolve, reject) => { /*eslint new-cap: 0*/
return new Promise((resolve, reject) => {
const results = [];
query.onerror = (event) => {
reject(new Error("Query failed: " + event.target.errorCode));
@@ -63,7 +63,7 @@ function selectQuery(store, keyRange, resultMapper) {
}
function promiseifyTxn(txn) {
return new q.Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
txn.oncomplete = function(event) {
resolve(event);
};
@@ -74,7 +74,7 @@ function promiseifyTxn(txn) {
}
function promiseifyRequest(req) {
return new q.Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
req.onsuccess = function(event) {
resolve(event);
};
@@ -113,19 +113,41 @@ LocalIndexedDBStoreBackend.prototype = {
*/
connect: function() {
if (this.db) {
return q();
console.log(
`LocalIndexedDBStoreBackend.connect: already connected`,
);
return Promise.resolve();
}
console.log(
`LocalIndexedDBStoreBackend.connect: connecting`,
);
const req = this.indexedDB.open(this._dbName, VERSION);
req.onupgradeneeded = (ev) => {
const db = ev.target.result;
const oldVersion = ev.oldVersion;
console.log(
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
);
if (oldVersion < 1) { // The database did not previously exist.
createDatabase(db);
}
// Expand as needed.
};
req.onblocked = () => {
console.log(
`can't yet open LocalIndexedDBStoreBackend because it is open elsewhere`,
);
};
console.log(
`LocalIndexedDBStoreBackend.connect: awaiting connection`,
);
return promiseifyRequest(req).then((ev) => {
console.log(
`LocalIndexedDBStoreBackend.connect: connected`,
);
this.db = ev.target.result;
// add a poorly-named listener for when deleteDatabase is called
@@ -143,13 +165,17 @@ LocalIndexedDBStoreBackend.prototype = {
* @return {Promise} Resolves on success
*/
_init: function() {
return q.all([
return Promise.all([
this._loadAccountData(),
this._loadSyncData(),
]).then(([accountData, syncData]) => {
console.log(
`LocalIndexedDBStoreBackend: loaded initial data`,
);
this._syncAccumulator.accumulate({
next_batch: syncData.nextBatch,
rooms: syncData.roomsData,
groups: syncData.groupsData,
account_data: {
events: accountData,
},
@@ -163,8 +189,32 @@ LocalIndexedDBStoreBackend.prototype = {
* @return {Promise} Resolved when the database is cleared.
*/
clearDatabase: function() {
console.log("Removing indexeddb instance: ", this._dbName);
return promiseifyRequest(this.indexedDB.deleteDatabase(this._dbName));
return new Promise((resolve, reject) => {
console.log(`Removing indexeddb instance: ${this._dbName}`);
const req = this.indexedDB.deleteDatabase(this._dbName);
req.onblocked = () => {
console.log(
`can't yet delete indexeddb ${this._dbName}` +
` because it is open elsewhere`,
);
};
req.onerror = (ev) => {
// in firefox, with indexedDB disabled, this fails with a
// DOMError. We treat this as non-fatal, so that we can still
// use the app.
console.warn(
`unable to delete js-sdk store indexeddb: ${ev.target.error}`,
);
resolve();
};
req.onsuccess = () => {
console.log(`Removed indexeddb instance: ${this._dbName}`);
resolve();
};
});
},
/**
@@ -180,18 +230,18 @@ LocalIndexedDBStoreBackend.prototype = {
if (copy === undefined) copy = true;
const data = this._syncAccumulator.getJSON();
if (!data.nextBatch) return q(null);
if (!data.nextBatch) return Promise.resolve(null);
if (copy) {
// We must deep copy the stored data so that the /sync processing code doesn't
// corrupt the internal state of the sync accumulator (it adds non-clonable keys)
return q(utils.deepCopy(data));
return Promise.resolve(utils.deepCopy(data));
} else {
return q(data);
return Promise.resolve(data);
}
},
setSyncData: function(syncData) {
return q().then(() => {
return Promise.resolve().then(() => {
this._syncAccumulator.accumulate(syncData);
});
},
@@ -199,10 +249,12 @@ LocalIndexedDBStoreBackend.prototype = {
syncToDatabase: function(userTuples) {
const syncData = this._syncAccumulator.getJSON();
return q.all([
return Promise.all([
this._persistUserPresenceEvents(userTuples),
this._persistAccountData(syncData.accountData),
this._persistSyncData(syncData.nextBatch, syncData.roomsData),
this._persistSyncData(
syncData.nextBatch, syncData.roomsData, syncData.groupsData,
),
]);
},
@@ -210,17 +262,19 @@ LocalIndexedDBStoreBackend.prototype = {
* Persist rooms /sync data along with the next batch token.
* @param {string} nextBatch The next_batch /sync value.
* @param {Object} roomsData The 'rooms' /sync data from a SyncAccumulator
* @param {Object} groupsData The 'groups' /sync data from a SyncAccumulator
* @return {Promise} Resolves if the data was persisted.
*/
_persistSyncData: function(nextBatch, roomsData) {
_persistSyncData: function(nextBatch, roomsData, groupsData) {
console.log("Persisting sync data up to ", nextBatch);
return q.try(() => {
return Promise.try(() => {
const txn = this.db.transaction(["sync"], "readwrite");
const store = txn.objectStore("sync");
store.put({
clobber: "-", // constant key so will always clobber
nextBatch: nextBatch,
roomsData: roomsData,
groupsData: groupsData,
}); // put == UPSERT
return promiseifyTxn(txn);
});
@@ -233,7 +287,7 @@ LocalIndexedDBStoreBackend.prototype = {
* @return {Promise} Resolves if the events were persisted.
*/
_persistAccountData: function(accountData) {
return q.try(() => {
return Promise.try(() => {
const txn = this.db.transaction(["accountData"], "readwrite");
const store = txn.objectStore("accountData");
for (let i = 0; i < accountData.length; i++) {
@@ -252,7 +306,7 @@ LocalIndexedDBStoreBackend.prototype = {
* @return {Promise} Resolves if the users were persisted.
*/
_persistUserPresenceEvents: function(tuples) {
return q.try(() => {
return Promise.try(() => {
const txn = this.db.transaction(["users"], "readwrite");
const store = txn.objectStore("users");
for (const tuple of tuples) {
@@ -272,7 +326,7 @@ LocalIndexedDBStoreBackend.prototype = {
* @return {Promise<Object[]>} A list of presence events in their raw form.
*/
getUserPresenceEvents: function() {
return q.try(() => {
return Promise.try(() => {
const txn = this.db.transaction(["users"], "readonly");
const store = txn.objectStore("users");
return selectQuery(store, undefined, (cursor) => {
@@ -286,7 +340,13 @@ LocalIndexedDBStoreBackend.prototype = {
* @return {Promise<Object[]>} A list of raw global account events.
*/
_loadAccountData: function() {
return q.try(() => {
console.log(
`LocalIndexedDBStoreBackend: loading account data`,
);
return Promise.try(() => {
console.log(
`LocalIndexedDBStoreBackend: loaded account data`,
);
const txn = this.db.transaction(["accountData"], "readonly");
const store = txn.objectStore("accountData");
return selectQuery(store, undefined, (cursor) => {
@@ -300,7 +360,13 @@ LocalIndexedDBStoreBackend.prototype = {
* @return {Promise<Object>} An object with "roomsData" and "nextBatch" keys.
*/
_loadSyncData: function() {
return q.try(() => {
console.log(
`LocalIndexedDBStoreBackend: loaded sync data`,
);
return Promise.try(() => {
console.log(
`LocalIndexedDBStoreBackend: loaded sync data`,
);
const txn = this.db.transaction(["sync"], "readonly");
const store = txn.objectStore("sync");
return selectQuery(store, undefined, (cursor) => {
+10 -8
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import q from "q";
import Promise from 'bluebird';
/**
* An IndexedDB store backend where the actual backend sits in a web
@@ -40,6 +40,11 @@ const RemoteIndexedDBStoreBackend = function RemoteIndexedDBStoreBackend(
};
this._worker.onmessage = this._onWorkerMessage.bind(this);
// tell the worker the db name.
this._startPromise = this._doCmd('_setupWorker', [this._dbName]).then(() => {
console.log("IndexedDB worker is ready");
});
};
@@ -50,10 +55,7 @@ RemoteIndexedDBStoreBackend.prototype = {
* @return {Promise} Resolves if successfully connected.
*/
connect: function() {
return this._doCmd('_setupWorker', [this._dbName]).then(() => {
console.log("IndexedDB worker is ready");
return this._doCmd('connect');
});
return this._startPromise.then(() => this._doCmd('connect'));
},
/**
@@ -62,7 +64,7 @@ RemoteIndexedDBStoreBackend.prototype = {
* @return {Promise} Resolved when the database is cleared.
*/
clearDatabase: function() {
return this._doCmd('clearDatabase');
return this._startPromise.then(() => this._doCmd('clearDatabase'));
},
/**
@@ -94,9 +96,9 @@ RemoteIndexedDBStoreBackend.prototype = {
_doCmd: function(cmd, args) {
// wrap in a q so if the postMessage throws,
// the promise automatically gets rejected
return q().then(() => {
return Promise.resolve().then(() => {
const seq = this._nextSeq++;
const def = q.defer();
const def = Promise.defer();
this._inFlight[seq] = def;
+2 -2
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import q from "q";
import Promise from 'bluebird';
import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
/**
@@ -61,7 +61,7 @@ class IndexedDBStoreWorker {
// because it's a web worker and there is no window).
indexedDB, msg.args[0],
);
prom = q();
prom = Promise.resolve();
break;
case 'connect':
prom = this.backend.connect();
+8 -4
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import q from "q";
import Promise from 'bluebird';
import {MatrixInMemoryStore} from "./memory";
import utils from "../utils";
import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
@@ -115,12 +115,16 @@ utils.inherits(IndexedDBStore, MatrixInMemoryStore);
*/
IndexedDBStore.prototype.startup = function() {
if (this.startedUp) {
return q();
console.log(`IndexedDBStore.startup: already started`);
return Promise.resolve();
}
console.log(`IndexedDBStore.startup: connecting to backend`);
return this.backend.connect().then(() => {
console.log(`IndexedDBStore.startup: loading presence events`);
return this.backend.getUserPresenceEvents();
}).then((userPresenceEvents) => {
console.log(`IndexedDBStore.startup: processing presence events`);
userPresenceEvents.forEach(([userId, rawEvent]) => {
const u = new User(userId);
if (rawEvent) {
@@ -150,7 +154,7 @@ IndexedDBStore.prototype.deleteAllData = function() {
return this.backend.clearDatabase().then(() => {
console.log("Deleted indexeddb data.");
}, (err) => {
console.error("Failed to delete indexeddb data: ", err);
console.error(`Failed to delete indexeddb data: ${err}`);
throw err;
});
};
@@ -164,7 +168,7 @@ IndexedDBStore.prototype.save = function() {
if (now - this._syncTs > WRITE_DELAY_MS) {
return this._reallySave();
}
return q();
return Promise.resolve();
};
IndexedDBStore.prototype._reallySave = function() {
+35 -7
View File
@@ -19,9 +19,9 @@ limitations under the License.
* This is an internal module. See {@link MatrixInMemoryStore} for the public class.
* @module store/memory
*/
const utils = require("../utils");
const User = require("../models/user");
const q = require("q");
const utils = require("../utils");
const User = require("../models/user");
import Promise from 'bluebird';
/**
* Construct a new in-memory data store for the Matrix Client.
@@ -35,6 +35,9 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
this.rooms = {
// roomId: Room
};
this.groups = {
// groupId: Group
};
this.users = {
// userId: User
};
@@ -69,6 +72,31 @@ module.exports.MatrixInMemoryStore.prototype = {
this.syncToken = token;
},
/**
* Store the given room.
* @param {Group} group The group to be stored
*/
storeGroup: function(group) {
this.groups[group.groupId] = group;
},
/**
* Retrieve a group by its group ID.
* @param {string} groupId The group ID.
* @return {Group} The group or null.
*/
getGroup: function(groupId) {
return this.groups[groupId] || null;
},
/**
* Retrieve all known groups.
* @return {Group[]} A list of groups, which may be empty.
*/
getGroups: function() {
return utils.values(this.groups);
},
/**
* Store the given room.
* @param {Room} room The room to be stored. All properties must be stored.
@@ -284,7 +312,7 @@ module.exports.MatrixInMemoryStore.prototype = {
* @return {Promise} An immediately resolved promise.
*/
setSyncData: function(syncData) {
return q();
return Promise.resolve();
},
/**
@@ -297,7 +325,7 @@ module.exports.MatrixInMemoryStore.prototype = {
* @return {Promise} An immediately resolved promise.
*/
startup: function() {
return q();
return Promise.resolve();
},
/**
@@ -306,7 +334,7 @@ module.exports.MatrixInMemoryStore.prototype = {
* is no saved sync data.
*/
getSavedSync: function() {
return q(null);
return Promise.resolve(null);
},
/**
@@ -329,6 +357,6 @@ module.exports.MatrixInMemoryStore.prototype = {
this.accountData = {
// type : content
};
return q();
return Promise.resolve();
},
};
-17
View File
@@ -65,22 +65,6 @@ WebStorageSessionStore.prototype = {
return this.store.getItem(KEY_END_TO_END_ACCOUNT);
},
/**
* Store a flag indicating that we have announced the new device.
*/
setDeviceAnnounced: function() {
this.store.setItem(KEY_END_TO_END_ANNOUNCED, "true");
},
/**
* Check if the "device announced" flag is set
*
* @return {boolean} true if the "device announced" flag has been set.
*/
getDeviceAnnounced: function() {
return this.store.getItem(KEY_END_TO_END_ANNOUNCED) == "true";
},
/**
* Stores the known devices for a user.
* @param {string} userId The user's ID.
@@ -208,7 +192,6 @@ WebStorageSessionStore.prototype = {
};
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
const KEY_END_TO_END_ANNOUNCED = E2E_PREFIX + "announced";
const KEY_END_TO_END_DEVICE_SYNC_TOKEN = E2E_PREFIX + "device_sync_token";
const KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS = E2E_PREFIX + "device_tracking";
+29 -5
View File
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import q from "q";
import Promise from 'bluebird';
/**
* This is an internal module.
* @module store/stub
@@ -47,6 +47,30 @@ StubStore.prototype = {
this.fromToken = token;
},
/**
* No-op.
* @param {Group} group
*/
storeGroup: function(group) {
},
/**
* No-op.
* @param {string} groupId
* @return {null}
*/
getGroup: function(groupId) {
return null;
},
/**
* No-op.
* @return {Array} An empty array.
*/
getGroups: function() {
return [];
},
/**
* No-op.
* @param {Room} room
@@ -189,7 +213,7 @@ StubStore.prototype = {
* @return {Promise} An immediately resolved promise.
*/
setSyncData: function(syncData) {
return q();
return Promise.resolve();
},
/**
@@ -202,7 +226,7 @@ StubStore.prototype = {
* @return {Promise} An immediately resolved promise.
*/
startup: function() {
return q();
return Promise.resolve();
},
/**
@@ -211,7 +235,7 @@ StubStore.prototype = {
* is no saved sync data.
*/
getSavedSync: function() {
return q(null);
return Promise.resolve(null);
},
/**
@@ -220,7 +244,7 @@ StubStore.prototype = {
* @return {Promise} An immediately resolved promise.
*/
deleteAllData: function() {
return q();
return Promise.resolve();
},
};
+47
View File
@@ -72,10 +72,18 @@ class SyncAccumulator {
// coherent /sync response and know at what point they should be
// streaming from without losing events.
this.nextBatch = null;
// { ('invite'|'join'|'leave'): $groupId: { ... sync 'group' data } }
this.groups = {
invite: {},
join: {},
leave: {},
};
}
accumulate(syncResponse) {
this._accumulateRooms(syncResponse);
this._accumulateGroups(syncResponse);
this._accumulateAccountData(syncResponse);
this.nextBatch = syncResponse.next_batch;
}
@@ -336,6 +344,44 @@ class SyncAccumulator {
}
}
/**
* Accumulate incremental /sync group data.
* @param {Object} syncResponse the complete /sync JSON
*/
_accumulateGroups(syncResponse) {
if (!syncResponse.groups) {
return;
}
if (syncResponse.groups.invite) {
Object.keys(syncResponse.groups.invite).forEach((groupId) => {
this._accumulateGroup(
groupId, "invite", syncResponse.groups.invite[groupId],
);
});
}
if (syncResponse.groups.join) {
Object.keys(syncResponse.groups.join).forEach((groupId) => {
this._accumulateGroup(
groupId, "join", syncResponse.groups.join[groupId],
);
});
}
if (syncResponse.groups.leave) {
Object.keys(syncResponse.groups.leave).forEach((groupId) => {
this._accumulateGroup(
groupId, "leave", syncResponse.groups.leave[groupId],
);
});
}
}
_accumulateGroup(groupId, category, data) {
for (const cat of ['invite', 'join', 'leave']) {
delete this.groups[cat][groupId];
}
this.groups[category][groupId] = data;
}
/**
* Return everything under the 'rooms' key from a /sync response which
* represents all room data that should be stored. This should be paired
@@ -470,6 +516,7 @@ class SyncAccumulator {
return {
nextBatch: this.nextBatch,
roomsData: data,
groupsData: this.groups,
accountData: accData,
};
}
+268 -167
View File
@@ -24,9 +24,10 @@ limitations under the License.
* an alternative syncing API, we may want to have a proper syncing interface
* for HTTP and WS at some point.
*/
const q = require("q");
import Promise from 'bluebird';
const User = require("./models/user");
const Room = require("./models/room");
const Group = require('./models/group');
const utils = require("./utils");
const Filter = require("./filter");
const EventTimeline = require("./models/event-timeline");
@@ -50,11 +51,11 @@ function getFilterName(userId, suffix) {
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
}
function debuglog() {
function debuglog(...params) {
if (!DEBUG) {
return;
}
console.log(...arguments);
console.log(...params);
}
@@ -97,7 +98,7 @@ function SyncApi(client, opts) {
this._failedSyncCount = 0; // Number of consecutive failed /sync requests
if (client.getNotifTimelineSet()) {
reEmit(client, client.getNotifTimelineSet(),
client.reEmitter.reEmit(client.getNotifTimelineSet(),
["Room.timeline", "Room.timelineReset"]);
}
}
@@ -112,7 +113,7 @@ SyncApi.prototype.createRoom = function(roomId) {
pendingEventOrdering: this.opts.pendingEventOrdering,
timelineSupport: client.timelineSupport,
});
reEmit(client, room, ["Room.name", "Room.timeline", "Room.redaction",
client.reEmitter.reEmit(room, ["Room.name", "Room.timeline", "Room.redaction",
"Room.receipt", "Room.tags",
"Room.timelineReset",
"Room.localEchoUpdated",
@@ -122,6 +123,18 @@ SyncApi.prototype.createRoom = function(roomId) {
return room;
};
/**
* @param {string} groupId
* @return {Group}
*/
SyncApi.prototype.createGroup = function(groupId) {
const client = this.client;
const group = new Group(groupId);
client.reEmitter.reEmit(group, ["Group.profile", "Group.myMembership"]);
client.store.storeGroup(group);
return group;
};
/**
* @param {Room} room
* @private
@@ -131,13 +144,13 @@ SyncApi.prototype._registerStateListeners = function(room) {
// we need to also re-emit room state and room member events, so hook it up
// to the client now. We need to add a listener for RoomState.members in
// order to hook them correctly. (TODO: find a better way?)
reEmit(client, room.currentState, [
client.reEmitter.reEmit(room.currentState, [
"RoomState.events", "RoomState.members", "RoomState.newMember",
]);
room.currentState.on("RoomState.newMember", function(event, state, member) {
member.user = client.getUser(member.userId);
reEmit(
client, member,
client.reEmitter.reEmit(
member,
[
"RoomMember.name", "RoomMember.typing", "RoomMember.powerLevel",
"RoomMember.membership",
@@ -218,6 +231,8 @@ SyncApi.prototype.syncLeftRooms = function() {
room.recalculate(client.credentials.userId);
client.store.storeRoom(room);
client.emit("Room", room);
self._processEventsForNotifs(room, timelineEvents);
});
return rooms;
});
@@ -296,7 +311,7 @@ SyncApi.prototype.peek = function(roomId) {
client.store.storeRoom(peekRoom);
client.emit("Room", peekRoom);
self._peekPoll(roomId);
self._peekPoll(peekRoom);
return peekRoom;
});
};
@@ -311,22 +326,26 @@ SyncApi.prototype.stopPeeking = function() {
/**
* Do a peek room poll.
* @param {string} roomId
* @param {Room} peekRoom
* @param {string} token from= token
*/
SyncApi.prototype._peekPoll = function(roomId, token) {
if (this._peekRoomId !== roomId) {
debuglog("Stopped peeking in room %s", roomId);
SyncApi.prototype._peekPoll = function(peekRoom, token) {
if (this._peekRoomId !== peekRoom.roomId) {
debuglog("Stopped peeking in room %s", peekRoom.roomId);
return;
}
const self = this;
// FIXME: gut wrenching; hard-coded timeout values
this.client._http.authedRequest(undefined, "GET", "/events", {
room_id: roomId,
room_id: peekRoom.roomId,
timeout: 30 * 1000,
from: token,
}, undefined, 50 * 1000).done(function(res) {
if (self._peekRoomId !== peekRoom.roomId) {
debuglog("Stopped peeking in room %s", peekRoom.roomId);
return;
}
// We have a problem that we get presence both from /events and /sync
// however, /sync only returns presence for users in rooms
// you're actually joined to.
@@ -352,15 +371,15 @@ SyncApi.prototype._peekPoll = function(roomId, token) {
// strip out events which aren't for the given room_id (e.g presence)
const events = res.chunk.filter(function(e) {
return e.room_id === roomId;
return e.room_id === peekRoom.roomId;
}).map(self.client.getEventMapper());
const room = self.client.getRoom(roomId);
room.addLiveEvents(events);
self._peekPoll(roomId, res.end);
peekRoom.addLiveEvents(events);
self._peekPoll(peekRoom, res.end);
}, function(err) {
console.error("[%s] Peek poll failed: %s", roomId, err);
console.error("[%s] Peek poll failed: %s", peekRoom.roomId, err);
setTimeout(function() {
self._peekPoll(roomId, token);
self._peekPoll(peekRoom, token);
}, 30 * 1000);
});
};
@@ -480,15 +499,14 @@ SyncApi.prototype.retryImmediately = function() {
* @param {string} syncOptions.filterId
* @param {boolean} syncOptions.hasSyncedBefore
*/
SyncApi.prototype._sync = function(syncOptions) {
SyncApi.prototype._sync = async function(syncOptions) {
const client = this.client;
const self = this;
if (!this._running) {
debuglog("Sync no longer running: exiting.");
if (self._connectionReturnedDefer) {
self._connectionReturnedDefer.reject();
self._connectionReturnedDefer = null;
if (this._connectionReturnedDefer) {
this._connectionReturnedDefer.reject();
this._connectionReturnedDefer = null;
}
this._updateSyncState("STOPPED");
return;
@@ -544,126 +562,134 @@ SyncApi.prototype._sync = function(syncOptions) {
qps.timeout = 0;
}
let isCachedResponse = false;
let syncPromise;
let savedSync;
if (!syncOptions.hasSyncedBefore) {
// Don't do an HTTP hit to /sync. Instead, load up the persisted /sync data,
// if there is data there.
syncPromise = client.store.getSavedSync();
} else {
syncPromise = q(null);
savedSync = await client.store.getSavedSync();
}
syncPromise.then((savedSync) => {
if (savedSync) {
debuglog("sync(): not doing HTTP hit, instead returning stored /sync data");
isCachedResponse = true;
return {
next_batch: savedSync.nextBatch,
rooms: savedSync.roomsData,
account_data: {
events: savedSync.accountData,
},
};
} else {
let isCachedResponse = false;
let data;
if (savedSync) {
debuglog("sync(): not doing HTTP hit, instead returning stored /sync data");
isCachedResponse = true;
data = {
next_batch: savedSync.nextBatch,
rooms: savedSync.roomsData,
groups: savedSync.groupsData,
account_data: {
events: savedSync.accountData,
},
};
} else {
try {
//debuglog('Starting sync since=' + syncToken);
this._currentSyncRequest = client._http.authedRequest(
undefined, "GET", "/sync", qps, undefined, clientSideTimeoutMs,
);
return this._currentSyncRequest;
}
}).then(function(data) {
//debuglog('Completed sync, next_batch=' + data.next_batch);
// set the sync token NOW *before* processing the events. We do this so
// if something barfs on an event we can skip it rather than constantly
// polling with the same token.
client.store.setSyncToken(data.next_batch);
// Reset after a successful sync
self._failedSyncCount = 0;
// We need to wait until the sync data has been sent to the backend
// because it appears that the sync data gets modified somewhere in
// processing it in such a way as to make it no longer cloneable.
// XXX: Find out what is modifying it!
if (!isCachedResponse) {
// Don't give the store back its own cached data
return client.store.setSyncData(data).then(() => {
return data;
});
} else {
return q(data);
}
}).done((data) => {
try {
self._processSyncResponse(syncToken, data);
data = await this._currentSyncRequest;
} catch (e) {
// log the exception with stack if we have it, else fall back
// to the plain description
console.error("Caught /sync error", e.stack || e);
this._onSyncError(e, syncOptions);
return;
}
}
// emit synced events
const syncEventData = {
oldSyncToken: syncToken,
nextSyncToken: data.next_batch,
catchingUp: self._catchingUp,
};
//debuglog('Completed sync, next_batch=' + data.next_batch);
if (!syncOptions.hasSyncedBefore) {
self._updateSyncState("PREPARED", syncEventData);
syncOptions.hasSyncedBefore = true;
// set the sync token NOW *before* processing the events. We do this so
// if something barfs on an event we can skip it rather than constantly
// polling with the same token.
client.store.setSyncToken(data.next_batch);
// Reset after a successful sync
this._failedSyncCount = 0;
// We need to wait until the sync data has been sent to the backend
// because it appears that the sync data gets modified somewhere in
// processing it in such a way as to make it no longer cloneable.
// XXX: Find out what is modifying it!
if (!isCachedResponse) {
// Don't give the store back its own cached data
await client.store.setSyncData(data);
}
try {
await this._processSyncResponse(syncToken, data, isCachedResponse);
} catch(e) {
// log the exception with stack if we have it, else fall back
// to the plain description
console.error("Caught /sync error", e.stack || e);
}
// emit synced events
const syncEventData = {
oldSyncToken: syncToken,
nextSyncToken: data.next_batch,
catchingUp: this._catchingUp,
};
if (!syncOptions.hasSyncedBefore) {
this._updateSyncState("PREPARED", syncEventData);
syncOptions.hasSyncedBefore = true;
}
if (!isCachedResponse) {
// tell the crypto module to do its processing. It may block (to do a
// /keys/changes request).
if (this.opts.crypto) {
await this.opts.crypto.onSyncCompleted(syncEventData);
}
// keep emitting SYNCING -> SYNCING for clients who want to do bulk updates
if (!isCachedResponse) {
self._updateSyncState("SYNCING", syncEventData);
this._updateSyncState("SYNCING", syncEventData);
// tell databases that everything is now in a consistent state and can be
// saved (no point doing so if we only have the data we just got out of the
// store).
client.store.save();
// tell databases that everything is now in a consistent state and can be
// saved (no point doing so if we only have the data we just got out of the
// store).
client.store.save();
}
// Begin next sync
this._sync(syncOptions);
};
SyncApi.prototype._onSyncError = function(err, syncOptions) {
if (!this._running) {
debuglog("Sync no longer running: exiting");
if (this._connectionReturnedDefer) {
this._connectionReturnedDefer.reject();
this._connectionReturnedDefer = null;
}
this._updateSyncState("STOPPED");
return;
}
console.error("/sync error %s", err);
console.error(err);
// Begin next sync
self._sync(syncOptions);
}, function(err) {
if (!self._running) {
debuglog("Sync no longer running: exiting");
if (self._connectionReturnedDefer) {
self._connectionReturnedDefer.reject();
self._connectionReturnedDefer = null;
}
self._updateSyncState("STOPPED");
return;
}
console.error("/sync error %s", err);
console.error(err);
this._failedSyncCount++;
console.log('Number of consecutive failed sync requests:', this._failedSyncCount);
self._failedSyncCount++;
console.log('Number of consecutive failed sync requests:', self._failedSyncCount);
debuglog("Starting keep-alive");
// Note that we do *not* mark the sync connection as
// lost yet: we only do this if a keepalive poke
// fails, since long lived HTTP connections will
// go away sometimes and we shouldn't treat this as
// erroneous. We set the state to 'reconnecting'
// instead, so that clients can onserve this state
// if they wish.
self._startKeepAlives().done(function() {
self._sync(syncOptions);
});
self._currentSyncRequest = null;
// Transition from RECONNECTING to ERROR after a given number of failed syncs
self._updateSyncState(
self._failedSyncCount >= FAILED_SYNC_ERROR_THRESHOLD ?
"ERROR" : "RECONNECTING",
);
debuglog("Starting keep-alive");
// Note that we do *not* mark the sync connection as
// lost yet: we only do this if a keepalive poke
// fails, since long lived HTTP connections will
// go away sometimes and we shouldn't treat this as
// erroneous. We set the state to 'reconnecting'
// instead, so that clients can onserve this state
// if they wish.
this._startKeepAlives().then(() => {
this._sync(syncOptions);
});
this._currentSyncRequest = null;
// Transition from RECONNECTING to ERROR after a given number of failed syncs
this._updateSyncState(
this._failedSyncCount >= FAILED_SYNC_ERROR_THRESHOLD ?
"ERROR" : "RECONNECTING",
);
};
/**
@@ -673,8 +699,11 @@ SyncApi.prototype._sync = function(syncOptions) {
* @param {string} syncToken the old next_batch token sent to this
* sync request.
* @param {Object} data The response from /sync
* @param {bool} isCachedResponse True if this response is from our local cache
*/
SyncApi.prototype._processSyncResponse = function(syncToken, data) {
SyncApi.prototype._processSyncResponse = async function(
syncToken, data, isCachedResponse,
) {
const client = this.client;
const self = this;
@@ -685,6 +714,7 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
// account_data: { events: [] },
// device_lists: { changed: ["@user:server", ... ]},
// to_device: { events: [] },
// device_one_time_keys_count: { signed_curve25519: 42 },
// rooms: {
// invite: {
// $roomid: {
@@ -710,6 +740,19 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
// }
// }
// },
// groups: {
// invite: {
// $groupId: {
// inviter: $inviter,
// profile: {
// avatar_url: $avatarUrl,
// name: $groupName,
// },
// },
// },
// join: {},
// leave: {},
// },
// }
// TODO-arch:
@@ -739,7 +782,13 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
client.store.storeAccountDataEvents(events);
events.forEach(
function(accountDataEvent) {
if (accountDataEvent.getType() == 'm.push_rules') {
// XXX: This is awful: ignore push rules from our cached sync. We fetch the
// push rules before syncing so we actually have up-to-date ones. We do want
// to honour new push rules that come down the sync but synapse doesn't
// put new push rules in the sync stream when the base rules change, so
// if the base rules change, we do need to refresh. We therefore ignore
// the push rules in our cached sync response.
if (accountDataEvent.getType() == 'm.push_rules' && !isCachedResponse) {
client.pushRules = accountDataEvent.getContent();
}
client.emit("accountData", accountDataEvent);
@@ -777,6 +826,20 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
this._catchingUp = false;
}
if (data.groups) {
if (data.groups.invite) {
this._processGroupSyncEntry(data.groups.invite, 'invite');
}
if (data.groups.join) {
this._processGroupSyncEntry(data.groups.join, 'join');
}
if (data.groups.leave) {
this._processGroupSyncEntry(data.groups.leave, 'leave');
}
}
// the returned json structure is a bit crap, so make it into a
// nicer form (array) after applying sanity to make sure we don't fail
// on missing keys (on the off chance)
@@ -815,7 +878,7 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
});
// Handle joins
joinRooms.forEach(function(joinObj) {
await Promise.mapSeries(joinRooms, async function(joinObj) {
const room = joinObj.room;
const stateEvents = self._mapSyncEventsFormat(joinObj.state, room);
const timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room);
@@ -878,14 +941,10 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
}
if (limited) {
// save the old 'next_batch' token as the
// forward-pagination token for the previously-active
// timeline.
room.currentState.paginationToken = syncToken;
self._deregisterStateListeners(room);
room.resetLiveTimeline(
joinObj.timeline.prev_batch,
self.opts.canResetEntireTimeline(room.roomId),
self.opts.canResetEntireTimeline(room.roomId) ? null : syncToken,
);
// We have to assume any gap in any timeline is
@@ -912,12 +971,18 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
client.store.storeRoom(room);
client.emit("Room", room);
}
stateEvents.forEach(function(e) {
self._processEventsForNotifs(room, timelineEvents);
async function processRoomEvent(e) {
client.emit("event", e);
});
timelineEvents.forEach(function(e) {
client.emit("event", e);
});
if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) {
await self.opts.crypto.onCryptoEvent(e);
}
}
await Promise.mapSeries(stateEvents, processRoomEvent);
await Promise.mapSeries(timelineEvents, processRoomEvent);
ephemeralEvents.forEach(function(e) {
client.emit("event", e);
});
@@ -945,6 +1010,8 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
client.emit("Room", room);
}
self._processEventsForNotifs(room, timelineEvents);
stateEvents.forEach(function(e) {
client.emit("event", e);
});
@@ -971,10 +1038,20 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
}
// Handle device list updates
if (this.opts.crypto && data.device_lists && data.device_lists.changed) {
data.device_lists.changed.forEach((u) => {
this.opts.crypto.userDeviceListChanged(u);
});
if (data.device_lists) {
if (this.opts.crypto) {
await this.opts.crypto.handleDeviceListChanges(data.device_lists);
} else {
// FIXME if we *don't* have a crypto module, we still need to
// invalidate the device lists. But that would require a
// substantial bit of rework :/.
}
}
// Handle one_time_keys_count
if (this.opts.crypto && data.device_one_time_keys_count) {
const currentCount = data.device_one_time_keys_count.signed_curve25519 || 0;
this.opts.crypto.updateOneTimeKeyCount(currentCount);
}
};
@@ -1003,7 +1080,7 @@ SyncApi.prototype._startKeepAlives = function(delay) {
self._pokeKeepAlive();
}
if (!this._connectionReturnedDefer) {
this._connectionReturnedDefer = q.defer();
this._connectionReturnedDefer = Promise.defer();
}
return this._connectionReturnedDefer.promise;
};
@@ -1060,6 +1137,35 @@ SyncApi.prototype._pokeKeepAlive = function() {
});
};
/**
* @param {Object} groupsSection Groups section object, eg. response.groups.invite
* @param {string} sectionName Which section this is ('invite', 'join' or 'leave')
*/
SyncApi.prototype._processGroupSyncEntry = function(groupsSection, sectionName) {
// Processes entries from 'groups' section of the sync stream
for (const groupId of Object.keys(groupsSection)) {
const groupInfo = groupsSection[groupId];
let group = this.client.store.getGroup(groupId);
const isBrandNew = group === null;
if (group === null) {
group = this.createGroup(groupId);
}
if (groupInfo.profile) {
group.setProfile(
groupInfo.profile.name, groupInfo.profile.avatar_url,
);
}
if (groupInfo.inviter) {
group.setInviter({userId: groupInfo.inviter});
}
group.setMyMembership(sectionName);
if (isBrandNew) {
// Now we've filled in all the fields, emit the Group event
this.client.emit("Group", group);
}
}
};
/**
* @param {Object} obj
* @return {Object[]}
@@ -1121,7 +1227,7 @@ SyncApi.prototype._resolveInvites = function(room) {
const user = client.getUser(member.userId);
let promise;
if (user) {
promise = q({
promise = Promise.resolve({
avatar_url: user.avatarUrl,
displayname: user.displayName,
});
@@ -1186,20 +1292,33 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
// may make notifications appear which should have the right name.
room.recalculate(this.client.credentials.userId);
// execute the timeline events, this will begin to diverge the current state
// if the timeline has any state events in it.
// This also needs to be done before running push rules on the events as they need
// to be decorated with sender etc.
room.addLiveEvents(timelineEventList);
};
/**
* Takes a list of timelineEvents and adds and adds to _notifEvents
* as appropriate.
* This must be called after the room the events belong to has been stored.
*
* @param {Room} room
* @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index
* is earlier in time. Higher index is later.
*/
SyncApi.prototype._processEventsForNotifs = function(room, timelineEventList) {
// gather our notifications into this._notifEvents
if (client.getNotifTimelineSet()) {
if (this.client.getNotifTimelineSet()) {
for (let i = 0; i < timelineEventList.length; i++) {
const pushActions = client.getPushActionsForEvent(timelineEventList[i]);
const pushActions = this.client.getPushActionsForEvent(timelineEventList[i]);
if (pushActions && pushActions.notify &&
pushActions.tweaks && pushActions.tweaks.highlight) {
this._notifEvents.push(timelineEventList[i]);
}
}
}
// execute the timeline events, this will begin to diverge the current state
// if the timeline has any state events in it.
room.addLiveEvents(timelineEventList);
};
/**
@@ -1245,30 +1364,12 @@ SyncApi.prototype._onOnline = function() {
function createNewUser(client, userId) {
const user = new User(userId);
reEmit(client, user, [
client.reEmitter.reEmit(user, [
"User.avatarUrl", "User.displayName", "User.presence",
"User.currentlyActive", "User.lastPresenceTs",
]);
return user;
}
function reEmit(reEmitEntity, emittableEntity, eventNames) {
utils.forEach(eventNames, function(eventName) {
// setup a listener on the entity (the Room, User, etc) for this event
emittableEntity.on(eventName, function() {
// take the args from the listener and reuse them, adding the
// event name to the arg list so it works with .emit()
// Transformation Example:
// listener on "foo" => function(a,b) { ... }
// Re-emit on "thing" => thing.emit("foo", a, b)
const newArgs = [eventName];
for (let i = 0; i < arguments.length; i++) {
newArgs.push(arguments[i]);
}
reEmitEntity.emit(...newArgs);
});
});
}
/** */
module.exports = SyncApi;
+38 -24
View File
@@ -17,7 +17,7 @@ limitations under the License.
/** @module timeline-window */
const q = require("q");
import Promise from 'bluebird';
const EventTimeline = require("./models/event-timeline");
/**
@@ -95,10 +95,30 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
const self = this;
initialWindowSize = initialWindowSize || 20;
// given an EventTimeline, and an event index within it, initialise our
// given an EventTimeline, find the event we were looking for, and initialise our
// fields so that the event in question is in the middle of the window.
const initFields = function(timeline, eventIndex) {
const endIndex = Math.min(timeline.getEvents().length,
const initFields = function(timeline) {
let eventIndex;
const events = timeline.getEvents();
if (!initialEventId) {
// we were looking for the live timeline: initialise to the end
eventIndex = events.length;
} else {
for (let i = 0; i < events.length; i++) {
if (events[i].getId() == initialEventId) {
eventIndex = i;
break;
}
}
if (eventIndex === undefined) {
throw new Error("getEventTimeline result didn't include requested event");
}
}
const endIndex = Math.min(events.length,
eventIndex + Math.ceil(initialWindowSize / 2));
const startIndex = Math.max(0, endIndex - initialWindowSize);
self._start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex());
@@ -110,25 +130,19 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
// we already have the data we need, which is important to keep room-switching
// feeling snappy.
//
// TODO: ideally we'd spot getEventTimeline returning a resolved promise and
// skip straight to the find-event loop.
if (initialEventId) {
return this._client.getEventTimeline(this._timelineSet, initialEventId)
.then(function(tl) {
// make sure that our window includes the event
for (let i = 0; i < tl.getEvents().length; i++) {
if (tl.getEvents()[i].getId() == initialEventId) {
initFields(tl, i);
return;
}
}
throw new Error("getEventTimeline result didn't include requested event");
});
const prom = this._client.getEventTimeline(this._timelineSet, initialEventId);
if (prom.isFulfilled()) {
initFields(prom.value());
return Promise.resolve();
} else {
return prom.then(initFields);
}
} else {
// start with the most recent events
const tl = this._timelineSet.getLiveTimeline();
initFields(tl, tl.getEvents().length);
return q();
initFields(tl);
return Promise.resolve();
}
};
@@ -220,7 +234,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
if (!tl) {
debuglog("TimelineWindow: no timeline yet");
return q(false);
return Promise.resolve(false);
}
if (tl.pendingPaginate) {
@@ -240,20 +254,20 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
if (excess > 0) {
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
}
return q(true);
return Promise.resolve(true);
}
if (!makeRequest || requestLimit === 0) {
// todo: should we return something different to indicate that there
// might be more events out there, but we haven't found them yet?
return q(false);
return Promise.resolve(false);
}
// try making a pagination request
const token = tl.timeline.getPaginationToken(direction);
if (!token) {
debuglog("TimelineWindow: no token");
return q(false);
return Promise.resolve(false);
}
debuglog("TimelineWindow: starting request");
+18 -7
View File
@@ -71,6 +71,7 @@ const DEBUG = true; // set true to enable console logging.
* @param {Object} opts Config options.
* @param {string} opts.roomId The room ID for this call.
* @param {Object} opts.webRtc The WebRTC globals from the browser.
* @param {boolean} opts.forceTURN whether relay through TURN should be forced.
* @param {Object} opts.URL The URL global.
* @param {Array<Object>} opts.turnServers Optional. A list of TURN servers.
* @param {MatrixClient} opts.client The Matrix Client instance to send events to.
@@ -79,6 +80,7 @@ function MatrixCall(opts) {
this.roomId = opts.roomId;
this.client = opts.client;
this.webRtc = opts.webRtc;
this.forceTURN = opts.forceTURN;
this.URL = opts.URL;
// Array of Objects with urls, username, credential keys
this.turnServers = opts.turnServers || [];
@@ -1184,6 +1186,7 @@ const _createPeerConnection = function(self) {
}
const pc = new self.webRtc.RtcPeerConnection({
iceTransportPolicy: self.forceTURN ? 'relay' : undefined,
iceServers: servers,
});
pc.oniceconnectionstatechange = hookCallback(self, self._onIceConnectionStateChanged);
@@ -1219,6 +1222,8 @@ const _getScreenSharingConstraints = function(call) {
};
const _getUserMediaVideoContraints = function(callType) {
const isWebkit = !!global.window.navigator.webkitGetUserMedia;
switch (callType) {
case 'voice':
return {
@@ -1232,12 +1237,11 @@ const _getUserMediaVideoContraints = function(callType) {
deviceId: audioInput ? {exact: audioInput} : undefined,
}, video: {
deviceId: videoInput ? {exact: videoInput} : undefined,
mandatory: {
minWidth: 640,
maxWidth: 640,
minHeight: 360,
maxHeight: 360,
},
/* We want 640x360. Chrome will give it only if we ask exactly,
FF refuses entirely if we ask exactly, so have to ask for ideal
instead */
width: isWebkit ? { exact: 640 } : { ideal: 640 },
height: isWebkit ? { exact: 360 } : { ideal: 360 },
},
};
}
@@ -1292,9 +1296,11 @@ module.exports.setVideoInput = function(deviceId) { videoInput = deviceId; };
* Create a new Matrix call for the browser.
* @param {MatrixClient} client The client instance to use.
* @param {string} roomId The room the call is in.
* @param {Object?} options DEPRECATED optional options map.
* @param {boolean} options.forceTURN DEPRECATED whether relay through TURN should be forced. This option is deprecated - use opts.forceTURN when creating the matrix client since it's only possible to set this option on outbound calls.
* @return {MatrixCall} the call or null if the browser doesn't support calling.
*/
module.exports.createNewMatrixCall = function(client, roomId) {
module.exports.createNewMatrixCall = function(client, roomId, options) {
const w = global.window;
const doc = global.document;
if (!w || !doc) {
@@ -1344,12 +1350,17 @@ module.exports.createNewMatrixCall = function(client, roomId) {
!webRtc.RtcPeerConnection || !webRtc.getUserMedia) {
return null; // WebRTC is not supported.
}
const optionsForceTURN = options ? options.forceTURN : false;
const opts = {
webRtc: webRtc,
client: client,
URL: w.URL,
roomId: roomId,
turnServers: client.getTurnServers(),
// call level options
forceTURN: client._forceTURN || optionsForceTURN,
};
return new MatrixCall(opts);
};
Executable
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
set -ex
npm run lint
# install Olm so that we can run the crypto tests.
npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz
npm run test
npm run gendoc