Compare commits

..

337 Commits

Author SHA1 Message Date
Matthew Hodgson 425f862cf8 Merge pull request #205 from matrix-org/markjh/megolm
Update the olm library version to 1.3.0
2016-09-16 17:30:26 +01:00
Mark Haines 5d6256bede Merge pull request #209 from matrix-org/markjh/comment_upload_key
Comment what the logic in uploadKeys does
2016-09-16 16:34:37 +01:00
Mark Haines ff5b923e6f Spelling: s/cliamed/claimed/ 2016-09-16 16:31:00 +01:00
Mark Haines af7a9a68b8 Merge pull request #210 from matrix-org/markjh/echo_keys_proved
Include keysProved and keysClaimed in the local echo for events we send.
2016-09-16 15:45:22 +01:00
Mark Haines 905059d6da More comments explaining the keysClaimed/keysProved properties 2016-09-16 15:42:02 +01:00
Mark Haines 3bc56cf3f8 More comments on the local echo 2016-09-16 15:36:56 +01:00
Mark Haines 1feb7fc0ba Fix copy+paste 2016-09-16 15:32:46 +01:00
Mark Haines c2a40572a5 Include keysProved and keysClaimed in the local echo for events we send. 2016-09-16 15:30:22 +01:00
Mark Haines ee7d4d0521 Explain what happens to the old keys in olm 2016-09-16 14:43:22 +01:00
Mark Haines 6ab410ef6a Comment what the logic in uploadKeys does 2016-09-16 14:38:26 +01:00
Mark Haines 8235d966d6 Merge pull request #208 from matrix-org/markjh/upload_keys
Check if we need to upload new one-time keys every 10 minutes
2016-09-16 14:21:03 +01:00
Mark Haines c7b83f6ee6 More semicolons 2016-09-16 11:40:06 +01:00
Mark Haines 460f20a4ce Merge pull request #207 from matrix-org/markjh/variable_scoping
Reset oneTimeKey to null on each loop iteration.
2016-09-16 11:23:58 +01:00
Mark Haines da408f975e Check if we need to upload new one-time keys every 10 minutes 2016-09-16 11:22:36 +01:00
Mark Haines 9a98c3991a Reset onTimeKey to null on each loop iteration.
Otherwise we will use a value from a previous iteration of the loop.
2016-09-16 10:44:25 +01:00
Matthew Hodgson 6e0b2de99f fix lint 2016-09-16 03:19:20 +01:00
Matthew Hodgson 0633d7d3f6 track raw displayname on user objects 2016-09-16 03:18:47 +01:00
Matthew Hodgson 2765720b76 unbreak NPE where megolm's decryptEvent doesn't return a result 2016-09-15 20:09:41 +01:00
Mark Haines 71f23ffce1 Merge branch 'develop' into markjh/megolm
Conflicts:
	lib/crypto/algorithms/megolm.js
2016-09-15 17:10:02 +01:00
Mark Haines 1863af147d Merge pull request #206 from matrix-org/markjh/ed25519
Add getKeysProved and getKeysClaimed methods to MatrixEvent.
2016-09-15 17:07:52 +01:00
Mark Haines 0d5d74674e Remove spurious senderKey argument 2016-09-15 16:46:28 +01:00
Mark Haines 45ed0884df Document return type 2016-09-15 16:42:40 +01:00
Mark Haines 45e9f59fdc Poke jenkins 2016-09-15 16:40:02 +01:00
Mark Haines bde6a171f6 Add getKeysProved and getKeysClaimed methods to MatrixEvent.
These list the keys that sender of the event must have ownership
of and the keys of that the sender claims ownership of.

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

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

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

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

We should only be adding the raw event.

Fixes (hopefully) https://github.com/vector-im/vector-web/issues/1389.
2016-08-30 14:30:12 +01:00
Matthew Hodgson e18b446190 unbreak filter text 2016-08-30 01:30:23 +01:00
Matthew Hodgson 0848d4ed10 reemit Room.timeline events correctly 2016-08-30 01:13:32 +01:00
Matthew Hodgson 7514aea813 make most things work other than Room.timeline firing 2016-08-30 01:11:47 +01:00
Matthew Hodgson 58031ab21d fix things until they almost work again... 2016-08-30 00:36:52 +01:00
Matthew Hodgson c1c2ca3ec1 tweak doc; make it build 2016-08-29 23:18:19 +01:00
Matthew Hodgson b863a363da WIP refactor 2016-08-29 21:08:35 +01:00
Matthew Hodgson b42db46abd WIP refactor 2016-08-29 21:06:53 +01:00
Matthew Hodgson d46863e199 fix syntax 2016-08-28 23:44:10 +01:00
Matthew Hodgson 751ce421cd Merge branch 'develop' into matthew/filtered-timelines 2016-08-28 18:49:54 +01:00
Matthew Hodgson 74e1dccf50 0.5.6 2016-08-28 16:36:06 +01:00
Matthew Hodgson bf3eaa9eb7 Prepare changelog for v0.5.6 2016-08-28 16:29:34 +01:00
Richard van der Hoff b4f22310ea Reset megolm session when people join/leave the room 2016-08-26 11:24:59 +01:00
Richard van der Hoff 2da70ca024 Merge pull request #182 from matrix-org/rav/single_key_message
Put all of the megolm keys in one room message
2016-08-24 11:48:54 +01:00
Richard van der Hoff 42babbc595 Put all of the megolm keys in one room message
Avoid hitting the rate-limiter by putting all of the megolm keys in a single
event.
2016-08-24 11:46:22 +01:00
Richard van der Hoff ba4735d4a8 Merge pull request #181 from matrix-org/rav/fix_device_blocking
Reinstate device blocking for simple Olm
2016-08-24 10:22:18 +01:00
Richard van der Hoff 31e7addf2f Reinstate device blocking for simple Olm
Commit 4cde51b3 broke device blocking such that we were encrypting for all
devices, including blocked ones. Reinstate it, and add a test.
2016-08-24 09:26:12 +01:00
Richard van der Hoff ba339ffdad Merge pull request #180 from matrix-org/rav/receive_megolm_keys
support for unpacking megolm keys
2016-08-23 17:30:30 +01:00
Richard van der Hoff a05cbab7c6 Merge pull request #179 from matrix-org/rav/send_megolm_keys
Send out megolm keys when we start a megolm session
2016-08-23 17:30:12 +01:00
Richard van der Hoff e708e59b15 Add a TODO about batching events 2016-08-23 17:27:47 +01:00
Matthew Hodgson dd5878015a WIP filtered timelines 2016-08-23 14:31:47 +01:00
Richard van der Hoff c72f613afc Merge pull request #178 from matrix-org/rav/refactor_ensuresessions
Change the result structure for ensureOlmSessionsForUsers
2016-08-23 11:19:19 +01:00
Richard van der Hoff 1159e0911f support for unpacking megolm keys
This is incredibly hacky at the moment, pending the arrival of ephemeral
events, but it kinda works.
2016-08-22 18:24:46 +01:00
Richard van der Hoff 9f180179d5 rename m.key event to m.room_key
... because m.key is scary, or something
2016-08-22 18:13:11 +01:00
Richard van der Hoff 238700cbdb Send out megolm keys when we start a megolm session
For now, pending the arrival of SPEC-138, this happens via inline messages in
the room.
2016-08-22 17:59:22 +01:00
Richard van der Hoff df43b19510 Change the result structure for ensureOlmSessionsForUsers
Nothing was using the results (except the tests), and it's more useful to have
the devices we *do* have a session for than the ones we don't.
2016-08-22 17:44:37 +01:00
Richard van der Hoff e4bfb3ca32 Merge pull request #177 from matrix-org/rav/olmlib
Factor out a function for doing olm encryption
2016-08-22 16:34:17 +01:00
Richard van der Hoff 0234410b43 Factor out a function for doing olm encryption
Make a library file with some constants and a function to pack olm-encrypted
events (which we are going to use from megolm)
2016-08-22 10:22:12 +01:00
Richard van der Hoff 4877edb79b Merge pull request #175 from matrix-org/rav/refactor_deviceinfo
Move DeviceInfo and DeviceVerification to separate module
2016-08-22 10:16:48 +01:00
Richard van der Hoff 1c4ee62397 Merge pull request #176 from matrix-org/rav/asynchronous_encryption
Make encryption asynchronous
2016-08-22 10:16:37 +01:00
Richard van der Hoff 7ea7e5ac6c Move DeviceInfo and DeviceVerification to separate module 2016-08-19 16:18:54 +01:00
Richard van der Hoff 32fa51818b Make encryption asynchronous
We're going to need to send out a load of messages to distribute the megolm
keys; as a first step, deal with the asynchronicity this will require.
2016-08-19 16:18:33 +01:00
David Baker e0bd05a8c4 Fix lint 2016-08-18 23:58:06 +01:00
David Baker a25315a994 Merge pull request #167 from Half-Shot/presence_status
Added ability to set and get status_msg for presence.
2016-08-18 11:18:03 +01:00
Richard van der Hoff 03e493453b Merge pull request #174 from matrix-org/rav/fix_room_reference
Megolm: don't dereference nullable object
2016-08-18 10:31:45 +01:00
Richard van der Hoff 4d6f9da578 Megolm: don't dereference nullable object
It is possible for `room` to be null when passed to
MegolmEncryption.encryptMessage; we need to avoid dereferencing it. Instead,
make sure that the EncryptionAlgorithm knows about the roomId it is targeting,
and use that.

Replace the increasingly-long argument list on the EncryptionAlgorithm
constructor with a params list, and update DecryptionAlgorithm to match.
2016-08-17 16:21:37 +01:00
Richard van der Hoff 783b1feb70 Merge branch 'rav/group_e2e' into develop 2016-08-16 18:10:38 +01:00
Richard van der Hoff 9925b327b4 pr feedback
break long line into two statements
2016-08-16 18:09:37 +01:00
Richard van der Hoff f75287b6b9 Merge pull request #170 from matrix-org/dbkr/push_update_rules_expose_func
Update our push rules when they come down stream
2016-08-16 17:10:14 +01:00
David Baker cc72d35c6b Move definition
So we don't have to fudge the jsdoc to make the linter happy
2016-08-16 17:02:18 +01:00
Richard van der Hoff 89d8133ad2 Implement megolm encryption/decryption
Very early attempt at encryption/decryption implementation via megolm. You have
to c&p the keys manually.
2016-08-16 16:47:37 +01:00
Richard van der Hoff e56833c7b2 Merge pull request #172 from matrix-org/rav/refactor_crypto
Factor Olm encryption/decryption out to new classes
2016-08-16 15:26:37 +01:00
Richard van der Hoff 2c9f8ba598 Factor Olm encryption/decryption out to new classes
- to make way for alternative encryption algorithms. We now store an encryption
object for each room, rather than referring to sessionstore on each event.

Also a little light tidying to the jsdocs.
2016-08-16 15:12:28 +01:00
Richard van der Hoff 1f16bba342 Merge pull request #171 from matrix-org/rav/refactor_deviceinfo
Make DeviceInfo more useful, and refactor crypto methods to use it
2016-08-16 14:19:45 +01:00
Richard van der Hoff 4cde51b3ce Make DeviceInfo more useful, and refactor crypto methods to use it
This is a prerequisite for a forthcoming refactor of _encryptMessage out to a
separate class.
2016-08-16 13:58:56 +01:00
David Baker 267e009ae3 Make lint pass
Although with slightly redundant doc :/
2016-08-15 18:55:09 +01:00
David Baker 0ba1a1dabc Update our push rules when they come down stream
Also expose a useful function from pushprocessor.

Fixes https://github.com/vector-im/vector-web/issues/1495
2016-08-15 18:40:12 +01:00
Richard van der Hoff 6739da5acb Fix login
https://github.com/matrix-org/matrix-js-sdk/pull/168 was broken :/
2016-08-12 13:30:04 +01:00
David Baker b0d5e1d844 Merge pull request #169 from matrix-org/rav/move_login_to_base_apis
Move login and register methods into base-apis
2016-08-12 13:10:59 +01:00
David Baker ea6f526ef9 Merge pull request #168 from matrix-org/rav/clean_up_login
Remove defaultDeviceDisplayName from MatrixClient options
2016-08-12 13:10:18 +01:00
Richard van der Hoff 3a7b1c6dd4 Move login and register methods into base-apis
login no longer relies on fields within MatrixClient, so we can move it down
to BaseApis
2016-08-12 13:02:40 +01:00
Richard van der Hoff b98e421b8a Remove defaultDeviceDisplayName
We no longer rely on js-sdk setting the initial_device_display_name and
login_id on login, so remove them to make `login` simpler.
2016-08-12 12:51:26 +01:00
David Baker d9318a60e4 0.5.5 2016-08-11 17:24:20 +01:00
David Baker 2fd569a61a Prepare changelog for v0.5.5 2016-08-11 17:17:28 +01:00
Will Hunt 1bd5d12665 Fixed setPresence opts 2016-08-11 13:39:27 +01:00
Will Hunt 02de5e96ba Add presenceStatusMsg to User 2016-08-11 12:55:07 +01:00
Will Hunt bc56213010 Add status_msg to setPresence 2016-08-11 12:50:13 +01:00
David Baker 34919d1b96 Merge pull request #166 from matrix-org/rav/stop_sync_when_stopped
Make sure we actually stop the sync loop on logout
2016-08-11 10:31:29 +01:00
Richard van der Hoff cc08de9c64 Make sure we actually stop the sync loop on logout
I think this was only a problem in the edgiest of edge conditions, but it
certainly didn't look right.
2016-08-11 01:13:52 +01:00
Richard van der Hoff 6432a64442 Merge pull request #164 from matrix-org/dbkr/fix_user_level_zero
Zero is a valid power level
2016-08-05 14:29:49 +01:00
David Baker 1f18dabca0 Add unit test 2016-08-05 14:28:12 +01:00
David Baker f74b49de4b Zero is a valid power level
So testing truthiness will lead to incorrect behaviour.

https://github.com/vector-im/vector-web/issues/1620
2016-08-05 11:54:38 +01:00
Richard van der Hoff 6aeb265c19 Merge pull request #163 from matrix-org/rav/check_sig_on_device_keys
Verify e2e keys on download
2016-08-04 15:45:39 +01:00
Richard van der Hoff f10467e81f Verify e2e keys on download
Check the signature on downloaded e2e keys, and ignore those that don't match.
2016-08-04 15:33:29 +01:00
Richard van der Hoff 6001077c34 Merge pull request #162 from matrix-org/rav/refactor_matrix_client
Factor crypto stuff out of MatrixClient
2016-08-04 14:56:54 +01:00
Richard van der Hoff ad6eec329d Factor crypto stuff out of MatrixClient
Introduce a new Crypto class which encapsulates all of the the crypto-related
gubbins, replacing it with thin wrappers in MatrixClient.
2016-08-04 12:06:37 +01:00
Richard van der Hoff d9867ba458 Merge pull request #161 from matrix-org/rav/refactor_key_upload
Refactor device key upload
2016-08-04 12:05:41 +01:00
Richard van der Hoff 6dc7e624d3 Fix device key signing
Calculate the signature *before* we add the `signatures` key.
2016-08-04 11:25:38 +01:00
Richard van der Hoff 24957a1445 Refactor device key upload
Use another-json instead of awful manual json building. Sign the device keys at
the point of upload, instead of having to keep the signed string in
memory. Only upload device keys once (they are correctly merged with the
one-time keys by synapse).
2016-08-04 10:03:31 +01:00
Richard van der Hoff e2d67db5d4 Fix missing semicolon 2016-08-04 09:18:26 +01:00
David Baker 6c59966339 Merge pull request #158 from matrix-org/rav/devices_api
Wrappers for devices API
2016-08-03 16:12:23 +01:00
Richard van der Hoff b4223d3790 Merge pull request #160 from matrix-org/dbkr/deactivate_account
Add deactivate account function
2016-08-03 15:40:43 +01:00
David Baker 9f6d9208f2 changelog 2016-08-03 15:26:52 +01:00
David Baker 35d45f0280 Add deactivate account function 2016-08-03 15:26:05 +01:00
Richard van der Hoff c288e6c7ec Merge pull request #159 from matrix-org/rav/device_name_for_e2e_keys
client.listDeviceKeys: Expose device display name
2016-08-03 14:32:02 +01:00
Richard van der Hoff bb946c65d1 client.listDeviceKeys: Expose device display name 2016-08-03 14:13:31 +01:00
Richard van der Hoff 13fe22bc86 Wrappers for devices API 2016-08-03 14:11:19 +01:00
Richard van der Hoff f139e6e6c2 Merge pull request #157 from matrix-org/dbkr/logout_api
Add `logout`
2016-08-02 16:19:49 +01:00
David Baker 364fe3ba47 Oops, s/MatrixClient/MatrixBaseApis/ 2016-08-02 15:53:29 +01:00
David Baker 8b9b37cc91 Move to base APIs 2016-08-02 15:44:54 +01:00
David Baker 7895d1daa0 appease linter 2016-08-02 15:36:36 +01:00
David Baker 93a9f76f69 Add logout 2016-08-02 14:52:24 +01:00
Richard van der Hoff 4e2edfc771 Merge pull request #156 from matrix-org/dbkr/fix_email_registration
Fix email registration
2016-07-29 16:51:49 +01:00
David Baker f4d53e25cc Remove all the device_id setting from the JS SDK
As discussed, this makes things quite complicated, so conclusion is that's better to just let the app do this.
2016-07-29 16:45:22 +01:00
David Baker da324c020b lint 2016-07-29 14:47:24 +01:00
David Baker f63015e4c4 Fix email registration
This would cause the request to 400 in the new vector that opens after you clicked the link in the email, as per the comment.
2016-07-29 14:40:53 +01:00
David Baker 61cf53deee Merge pull request #155 from matrix-org/rav/refactor_matrix_client
Factor out MatrixClient methods to MatrixBaseApis
2016-07-29 10:32:41 +01:00
David Baker 57ff963fae Merge pull request #154 from matrix-org/rav/fix_crypto_test
Fix some broken tests
2016-07-29 10:24:10 +01:00
David Baker 32744b23a0 Merge pull request #153 from matrix-org/rav/fail_build_on_test_fail
make jenkins fail the build if the tests fail
2016-07-29 10:22:39 +01:00
Richard van der Hoff 6c25110682 Factor out MatrixClient methods to MatrixBaseApis
Starts work on a class which is intended to just wrap the Matrix apis with very
simple functions.

There is a lot more work to be done here. For now, I have just taken methods
which don't refer to anything in MatrixClient except _http. This excludes a
bunch of things which refer to $userId, as well as the login stuff because of
the deviceId stuff I've just added :/.

For now, it's an internal class. I don't really see any reason it can't be
exposed to applications, though.
2016-07-28 15:36:45 +01:00
Richard van der Hoff 188802c5d3 Fix some broken tests
A number of the tests appear to have been broken since 90c919e without anyone
noticing; fix them.
2016-07-28 14:30:33 +01:00
Richard van der Hoff a47e59f02c make jenkins fail the build if the tests fail 2016-07-28 14:18:34 +01:00
David Baker 59d7935934 Merge pull request #152 from matrix-org/rav/deviceId
deviceId-related fixes
2016-07-27 11:12:14 +01:00
Richard van der Hoff ba616d2a25 deviceId-related fixes
A couple of changes to support bigger changes in the react-sdk:

1. Add getDeviceId() to MatrixClient
2. Don't attempt to upload e2e keys if deviceId wasn't set.
2016-07-26 22:52:45 +01:00
David Baker dc07038a27 Merge pull request #151 from matrix-org/rav/device_id_in_login
/login, /register: Add device_id and initial_device_display_name
2016-07-21 13:13:29 +01:00
Richard van der Hoff dd064ba0a1 /login, /register: Add device_id and initial_device_display_name
To help test the forthcoming device_id support for /login and /register, add
the device_id and initial_device_display_name parameters to those calls. Allow
the app to specify the default device displayname when creating the client (as
well as the device_id).

Also, don't try initialising the Olm layer unless a userId is
provided. Currently this isn't a problem because react-sdk doesn't provide a
sessionStore when it doesn't provide a userId, but that is a bad thing to rely
on (and I am going to break it with a react-sdk PR).
2016-07-20 20:06:14 +01:00
Matthew Hodgson 3baea89c34 Merge pull request #150 from matrix-org/matthew/generic-account-data
Support global account_data
2016-07-20 16:02:58 +01:00
Matthew Hodgson 1412646a55 fix review feedback 2016-07-20 15:40:58 +01:00
Matthew Hodgson c00a830cbb fix nightmare bug where Room.accountData wasn't being emitted by Room objects 2016-07-20 11:59:38 +01:00
Matthew Hodgson fa28297add thinkos 2016-07-20 10:17:54 +01:00
Matthew Hodgson 58a68106bc generic account data support 2016-07-18 01:40:05 +01:00
David Baker ebd2ef6f95 Merge pull request #149 from matrix-org/dbkr/emit_more_presence_events
Add more events to User
2016-07-14 10:34:58 +01:00
David Baker 809492d45d Fix currently_active event
Need === undefined here to check the presence of the field
2016-07-14 10:33:02 +01:00
David Baker 385c5d5469 More detailed changelog 2016-07-14 10:31:49 +01:00
David Baker 9713ffedf2 Do the changelog
Do do do do do do do do do
Do do do do do do do do do
Do do do do do do do do do
Do do do do do do do do do
Do the changelog
2016-07-14 10:08:22 +01:00
David Baker ecb31b5aaf Add more events to User
There was no way of observing changes to fields like currentlyActive, so add this and add one for lastPresenceTs that will be fired whenever we get a presence event.
2016-07-14 09:38:50 +01:00
Richard van der Hoff cee9a954ec Bump olm to 1.0.0 2016-07-11 17:04:41 +01:00
David Baker fc55858aa3 Merge pull request #148 from matrix-org/dbkr/more_requesttokens
Add API calls for other requestToken endpoints
2016-07-08 17:53:55 +01:00
David Baker b689dbb9c0 Better function name 2016-07-08 17:52:27 +01:00
David Baker 7dbc03942a linty lint lint 2016-07-08 17:35:37 +01:00
David Baker 425039c5b5 Add changelog entry 2016-07-08 17:26:23 +01:00
David Baker 03d0aecc26 Add API calls for other requestToken endpoints 2016-07-08 17:24:59 +01:00
David Baker abf903246c Add dummy doc to appease linter 2016-07-07 18:02:12 +01:00
David Baker 3fd601bda4 Use === 2016-07-07 17:56:54 +01:00
David Baker 1896c0b62f Merge pull request #147 from matrix-org/dbkr/register_request_token
Add register-specific request token endpoint
2016-07-07 11:17:20 +01:00
David Baker aa36571981 PR feedback inc doccing params 2016-07-07 11:16:50 +01:00
David Baker abbe9d2bc7 Add register-specific request token endpoint
As per https://github.com/matrix-org/matrix-doc/pull/343
2016-07-06 15:19:39 +01:00
Kegsay dff84c46f0 Merge pull request #139 from alefteris/valid-spdx
Set a valid SPDX license identifier in package.json
2016-07-05 12:57:38 +01:00
David Baker 7614c6677c Merge pull request #145 from matrix-org/rav/crypto_event
Configure encryption on m.room.encryption events
2016-06-23 18:25:15 +01:00
Richard van der Hoff 3d2a970457 Check m.room.encryption is a state event
- just to be paranoid.
2016-06-23 18:19:59 +01:00
David Baker 50d60f5dd3 Merge pull request #146 from matrix-org/rav/device_blocking
Implement device blocking
2016-06-23 18:16:00 +01:00
Richard van der Hoff 90c919e7e4 Implement device blocking
We want to be able to 'block' devices, so that they are not sent copies of our
text. Implement that change, and exclude blocked devices when encrypting
messages.

THis changes the name of the 'deviceVerified' event to
'deviceVerificationChanged', but that just means that the UI won't update
correctly until the changes to react-sdk arrive.
2016-06-23 17:23:23 +01:00
Richard van der Hoff 583ddc3e57 Configure encryption on m.room.encryption events
This is the first step in having a cross-room "enable encryption" button.

If encryption is enabled, add an event handler which will set up encryption
when we receive an m.room.encryption event (either at initial /sync, or in
subsequent syncs.)
2016-06-23 17:19:44 +01:00
Richard van der Hoff 7ff1cf4e4a Merge pull request #144 from matrix-org/dbkr/set_visibility_doc
Clearer doc for setRoomDirectoryVisibility
2016-06-22 17:20:13 +01:00
David Baker 1556cc4479 Oops, include the param name 2016-06-22 16:17:51 +01:00
Richard van der Hoff 32f7ca55df Merge pull request #143 from matrix-org/rav/room_encryption_state
crypto: use memberlist to derive recipient list
2016-06-22 15:26:13 +01:00
David Baker ae3551ad78 Clearer doc for setRoomDirectoryVisibility 2016-06-22 14:27:54 +01:00
Richard van der Hoff d0e90cd8c9 crypto: use memberlist to derive recipient list
When we send an encrypted message with Olm, we need to know who to send it
to. Currently that is based on a user list passed to setRoomEncryption. We want
to be able to change the list as people join and leave the room; I also want to
not have to keep the member list in local storage.

So, use the member list at the point of enabling encryption to set up sessions,
and at the point of sending a message to encrypt the message.

Further work here includes:

 * updating the react-sdk not to bother setting the member list
 * monitoring for new users/devices in the room, and setting up new sessions
   with them
2016-06-21 17:53:11 +01:00
Richard van der Hoff cb6566a198 matrix-client-crypto-spec: reorder
Separate the helper functions from the tests, and order them by functionality
rather than by test order.

I've been struggling to find the tests among the helpers, and struggling to
find the helpers among the other helpers. Hopefully this will help.
2016-06-21 10:12:38 +01:00
David Baker 573a9140d3 Merge pull request #142 from matrix-org/rav/unverify_device
Support for marking devices as unverified
2016-06-17 16:59:22 +01:00
David Baker 6a02f39f0b Merge pull request #141 from matrix-org/rav/olm_as_optional_dependency
Add Olm as an optionalDependency
2016-06-17 16:58:18 +01:00
Richard van der Hoff 7604bcc49a Support for marking devices as unverified
Mostly because it's useful for testing, to be honest.
2016-06-17 16:22:22 +01:00
Richard van der Hoff 40c73e2079 Add Olm as an optionalDependency
We have optional support for olm, so it makes sense to add it as an
optionalDependency here.
2016-06-17 16:05:43 +01:00
Richard van der Hoff 7f113de790 changelog: really fix "unreleased" tag 2016-06-17 15:52:38 +01:00
Richard van der Hoff accb589892 changelog: fix "unreleased" tag 2016-06-17 15:52:12 +01:00
Richard van der Hoff 4347f432b3 Merge pull request #140 from matrix-org/dbkr/room_get_aliases
Add room.getAliases() and room.getCanonicalAlias()
2016-06-17 15:50:32 +01:00
David Baker 6d905563fc Oops, no ES6 here. Also long line. 2016-06-17 15:21:51 +01:00
David Baker e943bc46d8 Add room.getAliases() and room.getCanonicalAlias() 2016-06-17 15:15:23 +01:00
Thanos Lefteris 7896b06bd7 Set a valid SPDX license expression in package.json
Signed-off-by: Thanos Lefteris <alefteris@gmail.com>
2016-06-14 18:35:40 +03:00
David Baker 0f153fdaf7 Merge pull request #138 from matrix-org/rav/event_encryption
Change how MatrixEvent manages encrypted events
2016-06-09 18:59:43 +01:00
Richard van der Hoff 2e4a8f4fa5 Change how MatrixEvent manages encrypted events
Make `MatrixEvent.event` contain the *encrypted* event, and keep the plaintext
payload in a separate `_clearEvent` property.

This achieves several aims:

* means that we have a record of the actual object which was received over
  the wire, which can be shown by clients for 'view source'.

* makes sent and received encrypted MatrixEvents more consistent (previously
  sent ones had `encryptedType` and `encryptedContent` properties, whereas
  received ones threw away the ciphertext).

* Avoids having to make two MatrixEvents in the receive path, and copying
  fields from one to the other.

*Hopefully* most clients have followed the advice given in the jsdoc of not
relying on MatrixEvent.event to get at events. If they haven't, I guess they'll
break in the face of encrypted events.

(The pushprocessor didn't follow this advice, so needed some tweaks.)
2016-06-09 18:23:54 +01:00
Richard van der Hoff 53b9154fe2 Merge pull request #137 from matrix-org/rav/encrypt_exceptions
Catch exceptions when encrypting events
2016-06-09 18:18:09 +01:00
Richard van der Hoff ce59b72308 Catch exceptions when encrypting events
If an exception was thrown by the encryption process, the event would be
queued, but the exception would not be handled. This meant that the event got
stuck as a grey 'sending' event in the UI.

Fixing this correctly is slightly more complex than just handling the exception
correctly. A naive approach would mean that the event would be shown as a red
'unsent' message, and clicking 'resend' would then send the message *in the
clear*. Hence, move the encryption to _sendEvent, where it will be called again
when the event is resent.
2016-06-09 17:18:29 +01:00
Richard van der Hoff 4bf24f0fe4 Client: add some logging to help understand what is going on 2016-06-09 17:18:22 +01:00
Richard van der Hoff 23a38ae8f2 Merge pull request #136 from matrix-org/rav/device_verification
Support for marking devices as verified
2016-06-09 10:42:12 +01:00
Richard van der Hoff 0ab3446d81 Emit an event when a device is verified
We will probably want to share device verification across our own devices at
some point, so this will be useful in the future.
2016-06-08 21:22:48 +01:00
Richard van der Hoff adaf4bf92b Remove spurious TODO
we are doing this a different way
2016-06-08 18:34:15 +01:00
Richard van der Hoff 60519c4e6b client: add isEventSenderVerified()
Add a method which allows applications to check if the sender of an event is on
the list of verified senders.
2016-06-08 17:20:26 +01:00
Richard van der Hoff 21a62f37de Client: mark our own device as verified 2016-06-08 17:20:26 +01:00
Richard van der Hoff 9feeb0c580 Support for marking devices as verified
Add a 'verified' property to the response from MatrixClient.listDeviceKeys, and
add MatrixClient.setDeviceVerified to set it. Also changes the format of data
stored for user devices in the session store slightly (in a
backwards-compatible way).
2016-06-08 17:20:26 +01:00
Richard van der Hoff 7c3104b2ae client: fix bug marking all sent events as encrypted 2016-06-08 17:19:04 +01:00
Richard van der Hoff 5ccbc0bbc6 Merge pull request #134 from matrix-org/rav/e2e/1
Various matrix-client refactorings and fixes
2016-06-07 23:48:33 +01:00
Richard van der Hoff 9c47400a0c client: Document the type of the sessionStore 2016-06-07 23:44:12 +01:00
Richard van der Hoff 9cb032ec44 OlmDevice: factor out a bunch of boilerplate
Create some helper functions which help us reduce the boilerplate even further
2016-06-07 23:36:04 +01:00
Richard van der Hoff 8c6e2591d9 Factor out OlmDevice from client.js
MatrixClient contains quite a lot of boilerplate for manipulating the Olm
things, which quite nicely factors out to a separate object.
2016-06-07 19:09:47 +01:00
Richard van der Hoff 0c8c7cf77a matrix-client-crypto-spec: add some more tests
Tests for bob receiving two messages, and for bob sending a message back to
alice.
2016-06-07 19:09:47 +01:00
Richard van der Hoff 52edcc49c5 matrix-client-crypto-spec: different backends for ali and bob
Use different mock http backends for the two different clients, so that we can
better control what each of them is doing (in particular, this is a
prerequisite for having them both running /sync loops)
2016-06-07 19:09:47 +01:00
Richard van der Hoff 5eede573c4 matrix-client-crypto-spec: shut down test clients
Running clients stop the test runner exiting cleanly, so make sure we stop them
2016-06-07 19:09:47 +01:00
Richard van der Hoff e9d60a252b matrix-client-crypto.spec.js: replace callbacks with promises
The pyramid of doom was getting unmanageable, not to mention the difficulty of
diagnosing why tests were failing, so replace the callbacks with promises.
2016-06-07 19:09:47 +01:00
Richard van der Hoff 17ec7daf23 MatrixClient: refactor uploadKeys
rewrite uploadKeys to require less looping and not to use deferreds.
2016-06-07 19:09:47 +01:00
Richard van der Hoff b18a4ee16b client.js: Fix error handling in downloadKeys
Fix a bug in the error handling in downloadKeys: If the http request failed,
then the exception would get silently swallowed and the promise would never
resolve.

Also: tests!
2016-06-07 17:26:56 +01:00
Richard van der Hoff f38f983a46 spec: Factor MockStorageApi out of webstorage.spec.js
This is useful elsewhere.
2016-06-07 17:26:56 +01:00
Matthew Hodgson e85c1e1231 0.5.4 2016-06-02 18:07:07 +01:00
Matthew Hodgson 1381a0226d Prepare changelog for v0.5.4 2016-06-02 18:07:07 +01:00
Matthew Hodgson 4bec72a2bd make release work on OSX 2016-06-02 18:02:47 +01:00
Matthew Hodgson 11f02d2e24 fix https://github.com/vector-im/vector-web/issues/1039, for
literally the 4th time. unbreak tests, and fix camelcase hell.
2016-06-02 16:54:18 +01:00
Richard van der Hoff 09d08a5092 0.5.3 2016-06-02 13:34:51 +01:00
Richard van der Hoff 8c7d041628 Prepare changelog for v0.5.3 2016-06-02 13:34:36 +01:00
Matthew Hodgson 0421e69f14 revert brand param at reg time 2016-06-02 12:33:09 +01:00
Matthew Hodgson 8b35ddae0a add 'brand' parameter to register() 2016-06-02 11:46:02 +01:00
Matthew Hodgson 7243367a64 only clobber displayname & avatarurl from presence if set. fixes https://github.com/vector-im/vector-web/issues/1039. again. 2016-06-01 03:45:16 +01:00
Matthew Hodgson fb388f5d2d fix typoes 2016-05-17 21:45:27 +01:00
David Baker 0757a1dd1c Merge pull request #133 from matrix-org/dbkr/scalar
Add support for the openid interface
2016-05-06 15:56:33 +01:00
David Baker 06486f7ad0 Fix c+p fails 2016-05-06 14:28:26 +01:00
David Baker a9d8c58ea0 Add support for the openid interface 2016-05-06 13:58:10 +01:00
Paul Evans beec36d484 Merge pull request #129 from matrix-org/paul/bugfix-upload-content
Bugfix for HTTP upload content when running on node
2016-04-25 15:46:09 +01:00
Paul "LeoNerd" Evans 37ea7edaa0 Bugfix for HTTP upload content when running on node 2016-04-21 14:16:20 +01:00
Richard van der Hoff 2c40932080 0.5.2 2016-04-19 13:10:12 +01:00
Richard van der Hoff 3777b3e211 Prepare changelog for v0.5.2 2016-04-19 13:09:56 +01:00
Matthew Hodgson 2f7d7308a1 Merge pull request #128 from matrix-org/matthew/syjs-28
Track the absolute time that presence events are received, so that the relative lastActiveAgo value is meaningful.
2016-04-18 19:15:03 +01:00
Matthew Hodgson 0e606c6fe2 incorporate PR feedback 2016-04-18 14:26:59 +01:00
Matthew Hodgson 3af35c8209 Merge branch 'develop' into matthew/syjs-28 2016-04-18 01:34:11 +01:00
Matthew Hodgson a2aed99f56 track lastPresenceTs 2016-04-18 01:25:34 +01:00
Richard van der Hoff 523a684d3f Merge pull request #127 from matrix-org/rav/refactor_add_events
Refactor the addition of events to rooms
2016-04-17 18:11:02 +01:00
Richard van der Hoff dc386bab46 Fix debug flag name 2016-04-14 17:53:57 +01:00
Richard van der Hoff 69079a2f9a A handy hook script 2016-04-14 17:41:52 +01:00
Richard van der Hoff df33f7aceb Fix lint failures 2016-04-14 17:36:25 +01:00
Richard van der Hoff d87e5471fa Refactor the addition of events to rooms
... and add some sanity checks

Two things here:

1. Clean up the Room API for adding new events to the timeline. Where before
we had addEvents and addEventsToTimeline, whose purposes were unclear, we now
have addLiveEvents which must be used for adding events to the end of the live
timeline, and addEventsToTimeline which should be used for pagination (either
back-pagination of the live timeline, or pagination of an old timeline).

2. Add some sanity checks for the live timeline. Today we have seen problems
where somehow the live timeline had gained a forward pagination token, or the
live timeline had got joined to another timeline, leading to much confusion -
and I would like to notice these sooner.
2016-04-14 17:03:25 +01:00
Richard van der Hoff 90101c0340 Give timelines a name
The debug of "joined timeline [Object] to timeline [Object]" isn't terribly
helpful, so let's at least give them an identifier.
2016-04-14 16:11:48 +01:00
Richard van der Hoff 950fce80c8 Whitespace fix
remove trailing ws
2016-04-14 16:09:51 +01:00
Richard van der Hoff 9135c50b83 Merge pull request #126 from matrix-org/rav/clean_up_testexit
Clean up test shutdown
2016-04-14 15:54:56 +01:00
Richard van der Hoff 83bad6ee0d Fix lint error
semicolons rool
2016-04-14 13:52:34 +01:00
Richard van der Hoff 3404751eb9 Clean up test shutdown
Make sure that the integration tests actually kill off all of their timers, so
that jasmine exits cleanly.

This probably also fixes https://github.com/vector-im/vector-web/issues/1365.
2016-04-14 12:01:23 +01:00
Richard van der Hoff 0282021e09 Add a cachebuster to initial /sync
... in the hope of fending off weird firefox restore issues
2016-04-13 22:17:10 +01:00
Richard van der Hoff 526e1d59e9 Log sync token when starting sync loop
A little bit of debug that might help with
https://github.com/vector-im/vector-web/issues/1354
2016-04-13 14:08:59 +01:00
David Baker dbc3a9a500 Merge pull request #125 from matrix-org/dbkr/get_pushers
Add methods to get (and set) pushers
2016-04-12 13:25:17 +01:00
David Baker cff7c8a59f Add methods to get (and set) pushers 2016-04-12 13:11:00 +01:00
Matthew Hodgson 64b8047f01 Merge pull request #122 from matrix-org/matthew/preview_urls
URL previewing support
2016-04-11 17:35:53 +01:00
Matthew Hodgson 11e4760935 improve commentary 2016-04-08 22:00:07 +01:00
Richard van der Hoff c21d5634bb Merge pull request #124 from matrix-org/rav/limit_pagination
Avoid paginating forever in private rooms
2016-04-08 15:55:37 +01:00
Richard van der Hoff dc56d7f784 Fix typo
s/evnets/events/
2016-04-08 15:54:57 +01:00
Matthew Hodgson 1ff1064295 Merge branch 'develop' into matthew/preview_urls 2016-04-07 17:26:10 +01:00
Richard van der Hoff a493a0ddb3 Fix lint errors 2016-04-07 14:30:35 +01:00
Richard van der Hoff 7573171d05 Avoid paginating forever in private rooms
In TimelineWindow.paginate, keep a count of the number of API requests we have
made, and bail out if it gets too high, to ensure that we don't get stuck in a
loop of paginating right back to the start of the room.
2016-04-07 14:16:02 +01:00
Richard van der Hoff 989e7cf78b Merge pull request #123 from matrix-org/rav/no_recreate_filter
Fix a bug where we recreated sync filters
2016-04-06 18:35:01 +01:00
Richard van der Hoff 384c474800 fix failure of deepCompare to compare arrays
what a difference a character can make
2016-04-06 18:14:34 +01:00
Richard van der Hoff 1d2c705e13 Fix a bug where we recreated sync filters
Fix the object comparison used for client filters (JSON.stringify is
non-deterministic)
2016-04-06 15:56:32 +01:00
Matthew Hodgson c469ff4c8d oops, fix sig 2016-04-03 01:19:34 +01:00
Matthew Hodgson c7575f3f16 cache url preview results 2016-04-03 01:18:01 +01:00
Matthew Hodgson 415251dd70 WIP url previewing 2016-03-31 18:38:34 +01:00
Richard van der Hoff 458cc55dec Merge pull request #121 from matrix-org/rav/realtime_callbacks
Implement HTTP callbacks in realtime
2016-03-31 16:33:25 +01:00
Richard van der Hoff d2adb30ded Implement HTTP callbacks in realtime
Hopefully this will improve our recovery time after a laptop is suspended. The
idea is to treat the timeouts on the http apis as being in realtime, rather
than in elapsed time while the machine is awake.

To do this, we add a layer on top of window.setTimeout. We run a callback every
second, which then checks the wallclock time and runs any pending callbacks.
2016-03-31 13:51:18 +01:00
65 changed files with 119901 additions and 2491 deletions
+153 -1
View File
@@ -1,3 +1,155 @@
Changes in [0.5.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.6) (2016-08-28)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.5...v0.5.6)
* Put all of the megolm keys in one room message
[\#182](https://github.com/matrix-org/matrix-js-sdk/pull/182)
* Reinstate device blocking for simple Olm
[\#181](https://github.com/matrix-org/matrix-js-sdk/pull/181)
* support for unpacking megolm keys
[\#180](https://github.com/matrix-org/matrix-js-sdk/pull/180)
* Send out megolm keys when we start a megolm session
[\#179](https://github.com/matrix-org/matrix-js-sdk/pull/179)
* Change the result structure for ensureOlmSessionsForUsers
[\#178](https://github.com/matrix-org/matrix-js-sdk/pull/178)
* Factor out a function for doing olm encryption
[\#177](https://github.com/matrix-org/matrix-js-sdk/pull/177)
* Move DeviceInfo and DeviceVerification to separate module
[\#175](https://github.com/matrix-org/matrix-js-sdk/pull/175)
* Make encryption asynchronous
[\#176](https://github.com/matrix-org/matrix-js-sdk/pull/176)
* Added ability to set and get status_msg for presence.
[\#167](https://github.com/matrix-org/matrix-js-sdk/pull/167)
* Megolm: don't dereference nullable object
[\#174](https://github.com/matrix-org/matrix-js-sdk/pull/174)
* Implement megolm encryption/decryption
[\#173](https://github.com/matrix-org/matrix-js-sdk/pull/173)
* Update our push rules when they come down stream
[\#170](https://github.com/matrix-org/matrix-js-sdk/pull/170)
* Factor Olm encryption/decryption out to new classes
[\#172](https://github.com/matrix-org/matrix-js-sdk/pull/172)
* Make DeviceInfo more useful, and refactor crypto methods to use it
[\#171](https://github.com/matrix-org/matrix-js-sdk/pull/171)
* Move login and register methods into base-apis
[\#169](https://github.com/matrix-org/matrix-js-sdk/pull/169)
* Remove defaultDeviceDisplayName from MatrixClient options
[\#168](https://github.com/matrix-org/matrix-js-sdk/pull/168)
Changes in [0.5.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.5) (2016-08-11)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.4...v0.5.5)
* Add room.getAliases() and room.getCanonicalAlias
* Add API calls `/register/email/requestToken`, `/account/password/email/requestToken` and `/account/3pid/email/requestToken`
* Add `User.currentlyActive` and `User.lastPresenceTs` events for changes in fields on the User object
* Add `logout` and `deactivateAccount`
* Make sure we actually stop the sync loop on logout
[\#166](https://github.com/matrix-org/matrix-js-sdk/pull/166)
* Zero is a valid power level
[\#164](https://github.com/matrix-org/matrix-js-sdk/pull/164)
* Verify e2e keys on download
[\#163](https://github.com/matrix-org/matrix-js-sdk/pull/163)
* Factor crypto stuff out of MatrixClient
[\#162](https://github.com/matrix-org/matrix-js-sdk/pull/162)
* Refactor device key upload
[\#161](https://github.com/matrix-org/matrix-js-sdk/pull/161)
* Wrappers for devices API
[\#158](https://github.com/matrix-org/matrix-js-sdk/pull/158)
* Add deactivate account function
[\#160](https://github.com/matrix-org/matrix-js-sdk/pull/160)
* client.listDeviceKeys: Expose device display name
[\#159](https://github.com/matrix-org/matrix-js-sdk/pull/159)
* Add `logout`
[\#157](https://github.com/matrix-org/matrix-js-sdk/pull/157)
* Fix email registration
[\#156](https://github.com/matrix-org/matrix-js-sdk/pull/156)
* Factor out MatrixClient methods to MatrixBaseApis
[\#155](https://github.com/matrix-org/matrix-js-sdk/pull/155)
* Fix some broken tests
[\#154](https://github.com/matrix-org/matrix-js-sdk/pull/154)
* make jenkins fail the build if the tests fail
[\#153](https://github.com/matrix-org/matrix-js-sdk/pull/153)
* deviceId-related fixes
[\#152](https://github.com/matrix-org/matrix-js-sdk/pull/152)
* /login, /register: Add device_id and initial_device_display_name
[\#151](https://github.com/matrix-org/matrix-js-sdk/pull/151)
* Support global account_data
[\#150](https://github.com/matrix-org/matrix-js-sdk/pull/150)
* Add more events to User
[\#149](https://github.com/matrix-org/matrix-js-sdk/pull/149)
* Add API calls for other requestToken endpoints
[\#148](https://github.com/matrix-org/matrix-js-sdk/pull/148)
* Add register-specific request token endpoint
[\#147](https://github.com/matrix-org/matrix-js-sdk/pull/147)
* Set a valid SPDX license identifier in package.json
[\#139](https://github.com/matrix-org/matrix-js-sdk/pull/139)
* Configure encryption on m.room.encryption events
[\#145](https://github.com/matrix-org/matrix-js-sdk/pull/145)
* Implement device blocking
[\#146](https://github.com/matrix-org/matrix-js-sdk/pull/146)
* Clearer doc for setRoomDirectoryVisibility
[\#144](https://github.com/matrix-org/matrix-js-sdk/pull/144)
* crypto: use memberlist to derive recipient list
[\#143](https://github.com/matrix-org/matrix-js-sdk/pull/143)
* Support for marking devices as unverified
[\#142](https://github.com/matrix-org/matrix-js-sdk/pull/142)
* Add Olm as an optionalDependency
[\#141](https://github.com/matrix-org/matrix-js-sdk/pull/141)
* Add room.getAliases() and room.getCanonicalAlias()
[\#140](https://github.com/matrix-org/matrix-js-sdk/pull/140)
* Change how MatrixEvent manages encrypted events
[\#138](https://github.com/matrix-org/matrix-js-sdk/pull/138)
* Catch exceptions when encrypting events
[\#137](https://github.com/matrix-org/matrix-js-sdk/pull/137)
* Support for marking devices as verified
[\#136](https://github.com/matrix-org/matrix-js-sdk/pull/136)
* Various matrix-client refactorings and fixes
[\#134](https://github.com/matrix-org/matrix-js-sdk/pull/134)
Changes in [0.5.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.4) (2016-06-02)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.3...v0.5.4)
* Correct fix for https://github.com/vector-im/vector-web/issues/1039
* Make release.sh work on OSX
Changes in [0.5.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.3) (2016-06-02)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.2...v0.5.3)
* Add support for the openid interface
[\#133](https://github.com/matrix-org/matrix-js-sdk/pull/133)
* Bugfix for HTTP upload content when running on node
[\#129](https://github.com/matrix-org/matrix-js-sdk/pull/129)
* Ignore missing profile (displayname and avatar_url) fields on
presence events, rather than overwriting existing valid profile
data from membership events or elsewhere.
Fixes https://github.com/vector-im/vector-web/issues/1039
Changes in [0.5.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.2) (2016-04-19)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.1...v0.5.2)
* Track the absolute time that presence events are received, so that the
relative lastActiveAgo value is meaningful.
[\#128](https://github.com/matrix-org/matrix-js-sdk/pull/128)
* Refactor the addition of events to rooms
[\#127](https://github.com/matrix-org/matrix-js-sdk/pull/127)
* Clean up test shutdown
[\#126](https://github.com/matrix-org/matrix-js-sdk/pull/126)
* Add methods to get (and set) pushers
[\#125](https://github.com/matrix-org/matrix-js-sdk/pull/125)
* URL previewing support
[\#122](https://github.com/matrix-org/matrix-js-sdk/pull/122)
* Avoid paginating forever in private rooms
[\#124](https://github.com/matrix-org/matrix-js-sdk/pull/124)
* Fix a bug where we recreated sync filters
[\#123](https://github.com/matrix-org/matrix-js-sdk/pull/123)
* Implement HTTP timeouts in realtime
[\#121](https://github.com/matrix-org/matrix-js-sdk/pull/121)
Changes in [0.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.1) (2016-03-30)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.0...v0.5.1)
@@ -79,7 +231,7 @@ Improvements:
* Avoid getting stuck in a pagination loop when the server sends us only
messages we've already seen
(https://github.com/matrix-org/matrix-js-sdk/pull/96).
New methods:
* Add `MatrixClient.setPushRuleActions` to set the actions for a push
notification rule (https://github.com/matrix-org/matrix-js-sdk/pull/90)
+33 -28
View File
@@ -14,7 +14,7 @@ Copy ``dist/$VERSION/browser-matrix-$VERSION.js`` and add that as a ``<script>``
your page. There will be a global variable ``matrixcs`` attached to
``window`` through which you can access the SDK.
Please check [the working browser example](examples/browser) for more information.
Please check [the working browser example](examples/browser) for more information.
In Node.js
----------
@@ -78,7 +78,7 @@ are updated.
client.on("event", function(event) {
console.log(event.getType());
});
// Listen for typing changes
client.on("RoomMember.typing", function(event, member) {
if (member.typing) {
@@ -88,38 +88,43 @@ are updated.
console.log(member.name + " stopped typing.");
}
});
// start the client to setup the connection to the server
client.startClient();
```
### Promises or Callbacks
### Promises and Callbacks
The SDK supports *both* callbacks and Promises (Q). The convention
you'll see used is:
Most of the methods in the SDK are asynchronous: they do not directly return a
result, but instead return a [Promise](http://documentup.com/kriskowal/q/)
which will be fulfilled in the future.
The typical usage is something like:
```javascript
var promise = matrixClient.someMethod(arg1, arg2, callback);
```
The ``callback`` parameter is optional, so you could do:
```javascript
matrixClient.someMethod(arg1, arg2).then(function(err, result) {
matrixClient.someMethod(arg1, arg2).done(function(result) {
...
});
```
Alternatively, you could do:
Alternatively, if you have a Node.js-style ``callback(err, result)`` function,
you can pass the result of the promise into it with something like:
```javascript
matrixClient.someMethod(arg1, arg2, function(result) {
...
});
matrixClient.someMethod(arg1, arg2).nodeify(callback);
```
Methods which support this will be clearly marked as returning
``Promises``.
The main thing to note is that it is an error to discard the result of a
promise-returning function, as that will cause exceptions to go unobserved. If
you have nothing better to do with the result, just call ``.done()`` on it. See
http://documentup.com/kriskowal/q/#the-end for more information.
Methods which return a promise show this in their documentation.
Many methods in the SDK support *both* Node.js-style callbacks *and* Promises,
via an optional ``callback`` argument. The callback support is now deprecated:
new methods do not include a ``callback`` argument, and in the future it may be
removed from existing methods.
Examples
--------
@@ -147,10 +152,10 @@ core functionality of the SDK. These examples assume the SDK is setup like this:
});
}
});
matrixClient.startClient();
```
### Print out messages for all rooms
```javascript
@@ -166,7 +171,7 @@ core functionality of the SDK. These examples assume the SDK is setup like this:
"(%s) %s :: %s", room.name, event.getSender(), event.getContent().body
);
});
matrixClient.startClient();
```
@@ -198,10 +203,10 @@ Output:
);
}
});
matrixClient.startClient();
```
Output:
```
My Room
@@ -211,7 +216,7 @@ Output:
(join) Bob
(invite) @charlie:localhost
```
API Reference
=============
@@ -226,7 +231,7 @@ host the API reference from the source files like this:
$ cd .jsdoc
$ python -m SimpleHTTPServer 8005
```
Then visit ``http://localhost:8005`` to see the API docs.
Contributing
@@ -256,7 +261,7 @@ To run tests (Jasmine)::
```
$ npm test
```
To run linting:
```
$ npm run lint
+16305
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+16327
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+37345
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+41222
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
#
# pre-commit: script to run checks on a working copy before commit
#
# To use, symlink it into .git/hooks:
# ln -s ../../git-hooks/pre-commit .git/hooks
#
set -e
# create a temp dir
tmpdir=`mktemp -d`
trap 'rm -rf "$tmpdir"' EXIT
# get a copy of the index
git checkout-index --prefix="$tmpdir/" -a
# keep node_modules/.bin on the path
rootdir=`git rev-parse --show-toplevel`
export PATH="$rootdir/node_modules/.bin:$PATH"
# now run our checks
cd "$tmpdir"
npm run lint
+22 -4
View File
@@ -1,13 +1,31 @@
#!/bin/bash -l
export NVM_DIR="/home/jenkins/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 0.10
npm install
npm test
jshint --reporter=checkstyle -c .jshint lib spec > jshint.xml || echo "jshint finished with return code $?"
gjslint --unix_mode --disable 0131,0211,0200,0222,0212 --max_line_length 90 -r lib/ -r spec/ > gjslint.log || echo "gjslint finished with return code $?"
RC=0
function fail {
echo $@ >&2
RC=1
}
npm test || fail "npm test finished with return code $?"
jshint --reporter=checkstyle -c .jshint lib spec > jshint.xml ||
fail "jshint finished with return code $?"
gjslint --unix_mode --disable 0131,0211,0200,0222,0212 \
--max_line_length 90 \
-r lib/ -r spec/ > gjslint.log ||
fail "gjslint finished with return code $?"
# delete the old tarball, if it exists
rm -f matrix-js-sdk-*.tgz
npm pack
npm pack ||
fail "npm pack finished with return code $?"
exit $RC
+1019
View File
File diff suppressed because it is too large Load Diff
+720 -1220
View File
File diff suppressed because it is too large Load Diff
+672
View File
@@ -0,0 +1,672 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* olm.js wrapper
*
* @module crypto/OlmDevice
*/
var Olm = require("olm");
var utils = require("../utils");
/**
* Manages the olm cryptography functions. Each OlmDevice has a single
* OlmAccount and a number of OlmSessions.
*
* Accounts and sessions are kept pickled in a sessionStore.
*
* @constructor
* @alias module:crypto/OlmDevice
*
* @param {Object} sessionStore A store to be used for data in end-to-end
* crypto
*
* @property {string} deviceCurve25519Key Curve25519 key for the account
* @property {string} deviceEd25519Key Ed25519 key for the account
*/
function OlmDevice(sessionStore) {
this._sessionStore = sessionStore;
this._pickleKey = "DEFAULT_KEY";
var e2eKeys;
var account = new Olm.Account();
try {
_initialise_account(this._sessionStore, this._pickleKey, account);
e2eKeys = JSON.parse(account.identity_keys());
} finally {
account.free();
}
this.deviceCurve25519Key = e2eKeys.curve25519;
this.deviceEd25519Key = e2eKeys.ed25519;
// we don't bother stashing outboundgroupsessions in the sessionstore -
// instead we keep them here.
this._outboundGroupSessionStore = {};
}
function _initialise_account(sessionStore, pickleKey, account) {
var e2eAccount = sessionStore.getEndToEndAccount();
if (e2eAccount !== null) {
account.unpickle(pickleKey, e2eAccount);
return;
}
account.create();
var pickled = account.pickle(pickleKey);
sessionStore.storeEndToEndAccount(pickled);
}
/**
* @return {array} The version of Olm.
*/
OlmDevice.getOlmVersion = function() {
return Olm.get_library_version();
};
/**
* extract our OlmAccount from the session store and call the given function
*
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getAccount = function(func) {
var account = new Olm.Account();
try {
var pickledAccount = this._sessionStore.getEndToEndAccount();
account.unpickle(this._pickleKey, pickledAccount);
return func(account);
} finally {
account.free();
}
};
/**
* store our OlmAccount in the session store
*
* @param {OlmAccount} account
* @private
*/
OlmDevice.prototype._saveAccount = function(account) {
var pickledAccount = account.pickle(this._pickleKey);
this._sessionStore.storeEndToEndAccount(pickledAccount);
};
/**
* extract an OlmSession from the session store and call the given function
*
* @param {string} deviceKey
* @param {string} sessionId
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getSession = function(deviceKey, sessionId, func) {
var sessions = this._sessionStore.getEndToEndSessions(deviceKey);
var pickledSession = sessions[sessionId];
var session = new Olm.Session();
try {
session.unpickle(this._pickleKey, pickledSession);
return func(session);
} finally {
session.free();
}
};
/**
* store our OlmSession in the session store
*
* @param {string} deviceKey
* @param {OlmSession} session
* @private
*/
OlmDevice.prototype._saveSession = function(deviceKey, session) {
var pickledSession = session.pickle(this._pickleKey);
this._sessionStore.storeEndToEndSession(
deviceKey, session.session_id(), pickledSession
);
};
/**
* get an OlmUtility and call the given function
*
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getUtility = function(func) {
var utility = new Olm.Utility();
try {
return func(utility);
} finally {
utility.free();
}
};
/**
* Signs a message with the ed25519 key for this account.
*
* @param {string} message message to be signed
* @return {string} base64-encoded signature
*/
OlmDevice.prototype.sign = function(message) {
return this._getAccount(function(account) {
return account.sign(message);
});
};
/**
* Get the current (unused, unpublished) one-time keys for this account.
*
* @return {object} one time keys; an object with the single property
* <tt>curve25519</tt>, which is itself an object mapping key id to Curve25519
* key.
*/
OlmDevice.prototype.getOneTimeKeys = function() {
return this._getAccount(function(account) {
return JSON.parse(account.one_time_keys());
});
};
/**
* Get the maximum number of one-time keys we can store.
*
* @return {number} number of keys
*/
OlmDevice.prototype.maxNumberOfOneTimeKeys = function() {
return this._getAccount(function(account) {
return account.max_number_of_one_time_keys();
});
};
/**
* Marks all of the one-time keys as published.
*/
OlmDevice.prototype.markKeysAsPublished = function() {
var self = this;
this._getAccount(function(account) {
account.mark_keys_as_published();
self._saveAccount(account);
});
};
/**
* Generate some new one-time keys
*
* @param {number} numKeys number of keys to generate
*/
OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
var self = this;
this._getAccount(function(account) {
account.generate_one_time_keys(numKeys);
self._saveAccount(account);
});
};
/**
* Generate a new outbound session
*
* The new session will be stored in the sessionStore.
*
* @param {string} theirIdentityKey remote user's Curve25519 identity key
* @param {string} theirOneTimeKey remote user's one-time Curve25519 key
* @return {string} sessionId for the outbound session.
*/
OlmDevice.prototype.createOutboundSession = function(
theirIdentityKey, theirOneTimeKey
) {
var self = this;
return this._getAccount(function(account) {
var session = new Olm.Session();
try {
session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
self._saveSession(theirIdentityKey, session);
return session.session_id();
} finally {
session.free();
}
});
};
/**
* Generate a new inbound session, given an incoming message
*
* @param {string} theirDeviceIdentityKey remote user's Curve25519 identity key
* @param {number} message_type message_type field from the received message (must be 0)
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {string} decrypted payload
*
* @raises {Error} if the received message was not valid (for instance, it
* didn't use a valid one-time key).
*/
OlmDevice.prototype.createInboundSession = function(
theirDeviceIdentityKey, message_type, ciphertext
) {
if (message_type !== 0) {
throw new Error("Need message_type == 0 to create inbound session");
}
var self = this;
return this._getAccount(function(account) {
var session = new Olm.Session();
try {
session.create_inbound_from(account, theirDeviceIdentityKey, ciphertext);
account.remove_one_time_keys(session);
self._saveAccount(account);
var payloadString = session.decrypt(message_type, ciphertext);
self._saveSession(theirDeviceIdentityKey, session);
return payloadString;
} finally {
session.free();
}
});
};
/**
* Get a list of known session IDs for the given device
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @return {string[]} a list of known session ids for the device
*/
OlmDevice.prototype.getSessionIdsForDevice = function(theirDeviceIdentityKey) {
var sessions = this._sessionStore.getEndToEndSessions(
theirDeviceIdentityKey
);
return utils.keys(sessions);
};
/**
* Get the right olm session id for encrypting messages to the given identity key
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @return {string?} session id, or null if no established session
*/
OlmDevice.prototype.getSessionIdForDevice = function(theirDeviceIdentityKey) {
var sessionIds = this.getSessionIdsForDevice(theirDeviceIdentityKey);
if (sessionIds.length === 0) {
return null;
}
// Use the session with the lowest ID.
sessionIds.sort();
return sessionIds[0];
};
/**
* Get information on the active Olm sessions for a device.
* <p>
* Returns an array, with an entry for each active session. The first entry in
* the result will be the one used for outgoing messages. Each entry contains
* the keys 'hasReceivedMessage' (true if the session has received an incoming
* message and is therefore past the pre-key stage), and 'sessionId'.
*
* @param {string} deviceIdentityKey Curve25519 identity key for the device
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
*/
OlmDevice.prototype.getSessionInfoForDevice = function(deviceIdentityKey) {
var sessionIds = this.getSessionIdsForDevice(deviceIdentityKey);
sessionIds.sort();
var info = [];
function getSessionInfo(session) {
return {
hasReceivedMessage: session.has_received_message()
};
}
for (var i = 0; i < sessionIds.length; i++) {
var sessionId = sessionIds[i];
var res = this._getSession(deviceIdentityKey, sessionId, getSessionInfo);
res.sessionId = sessionId;
info.push(res);
}
return info;
};
/**
* Encrypt an outgoing message using an existing session
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {string} payloadString payload to be encrypted and sent
*
* @return {string} ciphertext
*/
OlmDevice.prototype.encryptMessage = function(
theirDeviceIdentityKey, sessionId, payloadString
) {
var self = this;
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
var res = session.encrypt(payloadString);
self._saveSession(theirDeviceIdentityKey, session);
return res;
});
};
/**
* Decrypt an incoming message using an existing session
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {number} message_type message_type field from the received message
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {object} Result, with keys <ul>
* <li><tt>matchesInbound</tt>: (boolean): true if the message was a pre-key
* message which matched an existing inbound session.</li>
* <li><tt>payload</tt>: (string): decrypted payload; null if the received
* message was not valid (for instance, it did not match this session).</li>
* </ul>
*/
OlmDevice.prototype.decryptMessage = function(
theirDeviceIdentityKey, sessionId, message_type, ciphertext
) {
var self = this;
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
var matchesInbound = (message_type === 0 &&
session.matches_inbound(ciphertext));
var payloadString = null;
try {
payloadString = session.decrypt(message_type, ciphertext);
} catch (e) {
console.log(
"Failed to decrypt with an existing session: " + e.message
);
// return null as the payload
}
self._saveSession(theirDeviceIdentityKey, session);
return {
matchesInbound: matchesInbound,
payload: payloadString,
};
});
};
// Outbound group session
// ======================
/**
* store an OutboundGroupSession in _outboundGroupSessionStore
*
* @param {Olm.OutboundGroupSession} session
* @private
*/
OlmDevice.prototype._saveOutboundGroupSession = function(session) {
var pickledSession = session.pickle(this._pickleKey);
this._outboundGroupSessionStore[session.session_id()] = pickledSession;
};
/**
* extract an OutboundGroupSession from _outboundGroupSessionStore and call the
* given function
*
* @param {string} sessionId
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
var pickled = this._outboundGroupSessionStore[sessionId];
if (pickled === null) {
throw new Error("Unknown outbound group session " + sessionId);
}
var session = new Olm.OutboundGroupSession();
try {
session.unpickle(this._pickleKey, pickled);
return func(session);
} finally {
session.free();
}
};
/**
* Generate a new outbound group session
*
* @return {string} sessionId for the outbound session.
*/
OlmDevice.prototype.createOutboundGroupSession = function() {
var session = new Olm.OutboundGroupSession();
try {
session.create();
this._saveOutboundGroupSession(session);
return session.session_id();
} finally {
session.free();
}
};
/**
* Encrypt an outgoing message with an outbound group session
*
* @param {string} sessionId the id of the outboundgroupsession
* @param {string} payloadString payload to be encrypted and sent
*
* @return {string} ciphertext
*/
OlmDevice.prototype.encryptGroupMessage = function(sessionId, payloadString) {
var self = this;
return this._getOutboundGroupSession(sessionId, function(session) {
var res = session.encrypt(payloadString);
self._saveOutboundGroupSession(session);
return res;
});
};
/**
* Get the session keys for an outbound group session
*
* @param {string} sessionId the id of the outbound group session
*
* @return {{chain_index: number, key: string}} current chain index, and
* base64-encoded secret key.
*/
OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
return this._getOutboundGroupSession(sessionId, function(session) {
return {
chain_index: session.message_index(),
key: session.session_key(),
};
});
};
// Inbound group session
// =====================
/**
* store an InboundGroupSession in the session store
*
* @param {string} roomId
* @param {string} senderCurve25519Key
* @param {string} sessionId
* @param {Olm.InboundGroupSession} session
* @param {object} keysClaimed Other keys the sender claims.
* @private
*/
OlmDevice.prototype._saveInboundGroupSession = function(
roomId, senderCurve25519Key, sessionId, session, keysClaimed
) {
var r = {
room_id: roomId,
session: session.pickle(this._pickleKey),
keysClaimed: keysClaimed,
};
this._sessionStore.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, JSON.stringify(r)
);
};
/**
* extract an InboundGroupSession from the session store and call the given function
*
* @param {string} roomId
* @param {string} senderKey
* @param {string} sessionId
* @param {function} func
* @return {object} Object with two keys "result": result of func, "exists"
* whether the session exists. if the session doesn't exist then the function
* isn't called and the "result" is undefined.
* @private
*/
OlmDevice.prototype._getInboundGroupSession = function(
roomId, senderKey, sessionId, func
) {
var r = this._sessionStore.getEndToEndInboundGroupSession(
senderKey, sessionId
);
if (r === null) {
return {sessionExists: false};
}
r = JSON.parse(r);
// check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room.
if (roomId !== r.room_id) {
throw new Error(
"Mismatched room_id for inbound group session (expected " + r.room_id +
", was " + roomId + ")"
);
}
var session = new Olm.InboundGroupSession();
try {
session.unpickle(this._pickleKey, r.session);
return {
sessionExists: true,
result: func(session),
keysProved: {curve25519: senderKey},
keysClaimed: r.keysClaimed || {},
};
} finally {
session.free();
}
};
/**
* Add an inbound group session to the session store
*
* @param {string} roomId room in which this session will be used
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {string} sessionKey base64-encoded secret key at index chainIndex
* @param {number} chainIndex index at which sessionKey applies
*/
OlmDevice.prototype.addInboundGroupSession = function(
roomId, senderKey, sessionId, sessionKey, chainIndex
) {
var self = this;
var session = new Olm.InboundGroupSession();
try {
session.create(sessionKey);
if (sessionId != session.session_id()) {
throw new Error(
"Mismatched group session ID from senderKey: " + senderKey
);
}
self._saveInboundGroupSession(roomId, senderKey, sessionId, session);
} finally {
session.free();
}
};
/**
* Decrypt a received message with an inbound group session
*
* @param {string} roomId room in which the message was received
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {string} body base64-encoded body of the encrypted message
*
* @return {object} {result: "plaintext"|undefined, sessionExists: Boolean}
*/
OlmDevice.prototype.decryptGroupMessage = function(
roomId, senderKey, sessionId, body
) {
var self = this;
function decrypt(session) {
var res = session.decrypt(body);
self._saveInboundGroupSession(
roomId, senderKey, sessionId, session
);
return res;
}
return this._getInboundGroupSession(
roomId, senderKey, sessionId, decrypt
);
};
// Utilities
// =========
/**
* Verify an ed25519 signature.
*
* @param {string} key ed25519 key
* @param {string} message message which was signed
* @param {string} signature base64-encoded signature to be checked
*
* @raises {Error} if there is a problem with the verification. If the key was
* too small then the message will be "OLM.INVALID_BASE64". If the signature
* was invalid then the message will be "OLM.BAD_MESSAGE_MAC".
*/
OlmDevice.prototype.verifySignature = function(
key, message, signature
) {
this._getUtility(function(util) {
util.ed25519_verify(key, message, signature);
});
};
/** */
module.exports = OlmDevice;
+165
View File
@@ -0,0 +1,165 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* Internal module. Defines the base classes of the encryption implementations
*
* @module crypto/algorithms/base
*/
var utils = require("../../utils");
/**
* map of registered encryption algorithm classes. A map from string to {@link
* module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} class
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
*/
module.exports.ENCRYPTION_CLASSES = {};
/**
* map of registered encryption algorithm classes. Map from string to {@link
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} class
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
*/
module.exports.DECRYPTION_CLASSES = {};
/**
* base type for encryption implementations
*
* @constructor
* @alias module:crypto/algorithms/base.EncryptionAlgorithm
*
* @param {object} params parameters
* @param {string} params.deviceId The identifier for this device.
* @param {module:crypto} params.crypto crypto core
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
* @param {string} params.roomId The ID of the room we will be sending to
*/
var EncryptionAlgorithm = function(params) {
this._deviceId = params.deviceId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._baseApis = params.baseApis;
this._roomId = params.roomId;
};
/** */
module.exports.EncryptionAlgorithm = EncryptionAlgorithm;
/**
* 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 a new device announces itself in the room
*
* @param {string} userId owner of the device
* @param {string} deviceId deviceId of the device
*/
EncryptionAlgorithm.prototype.onNewDevice = function(userId, deviceId) {};
/**
* base type for decryption implementations
*
* @constructor
* @alias module:crypto/algorithms/base.DecryptionAlgorithm
*
* @param {object} params parameters
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
*/
var DecryptionAlgorithm = function(params) {
this._olmDevice = params.olmDevice;
};
/** */
module.exports.DecryptionAlgorithm = DecryptionAlgorithm;
/**
* Decrypt an event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent
* @abstract
*
* @param {object} event raw event
*
* @return {object} decrypted payload (with properties 'type', 'content')
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
/**
* Handle a key event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent
*
* @param {module:models/event.MatrixEvent} event key event
*/
DecryptionAlgorithm.prototype.onRoomKeyEvent = function(params) {
// ignore by default
};
/**
* Exception thrown when decryption fails
*
* @constructor
* @param {string} msg message describing the problem
* @extends Error
*/
module.exports.DecryptionError = function(msg) {
this.message = msg;
};
utils.inherits(module.exports.DecryptionError, Error);
/**
* Registers an encryption/decryption class for a particular algorithm
*
* @param {string} algorithm algorithm tag to register for
*
* @param {class} encryptor {@link
* module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm}
* implementation
*
* @param {class} decryptor {@link
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm}
* implementation
*/
module.exports.registerAlgorithm = function(algorithm, encryptor, decryptor) {
module.exports.ENCRYPTION_CLASSES[algorithm] = encryptor;
module.exports.DECRYPTION_CLASSES[algorithm] = decryptor;
};
+40
View File
@@ -0,0 +1,40 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module crypto/algorithms
*/
var base = require("./base");
require("./olm");
require("./megolm");
/**
* @see module:crypto/algorithms/base.ENCRYPTION_CLASSES
*/
module.exports.ENCRYPTION_CLASSES = base.ENCRYPTION_CLASSES;
/**
* @see module:crypto/algorithms/base.DECRYPTION_CLASSES
*/
module.exports.DECRYPTION_CLASSES = base.DECRYPTION_CLASSES;
/**
* @see module:crypto/algorithms/base.DecryptionError
*/
module.exports.DecryptionError = base.DecryptionError;
+420
View File
@@ -0,0 +1,420 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* Defines m.olm encryption/decryption
*
* @module crypto/algorithms/megolm
*/
var q = require("q");
var utils = require("../../utils");
var olmlib = require("../olmlib");
var base = require("./base");
/**
* Megolm encryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.EncryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.EncryptionAlgorithm}
*/
function MegolmEncryption(params) {
base.EncryptionAlgorithm.call(this, params);
this._prepPromise = null;
this._outboundSessionId = null;
this._discardNewSession = false;
// devices which have joined since we last sent a message.
// userId -> {deviceId -> true}, or
// userId -> true
this._devicesPendingKeyShare = {};
this._sharePromise = null;
}
utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
/**
* @private
*
* @param {module:models/room} room
*
* @return {module:client.Promise} Promise which resolves to the megolm
* sessionId when setup is complete.
*/
MegolmEncryption.prototype._ensureOutboundSession = function(room) {
var self = this;
if (this._prepPromise) {
// prep already in progress
return this._prepPromise;
}
var sessionId = this._outboundSessionId;
// need to make a brand new session?
if (!sessionId) {
this._prepPromise = this._prepareNewSession(room).
finally(function() {
self._prepPromise = null;
});
return this._prepPromise;
}
if (this._sharePromise) {
// key share already in progress
return this._sharePromise;
}
// prep already done, but check for new devices
var shareMap = this._devicesPendingKeyShare;
this._devicesPendingKeyShare = {};
// check each user is (still) a member of the room
for (var userId in shareMap) {
if (!shareMap.hasOwnProperty(userId)) {
continue;
}
// XXX what about rooms where invitees can see the content?
var member = room.getMember(userId);
if (member.membership !== "join") {
delete shareMap[userId];
}
}
this._sharePromise = this._shareKeyWithDevices(
sessionId, shareMap
).finally(function() {
self._sharePromise = null;
}).then(function() {
return sessionId;
});
return this._sharePromise;
};
/**
* @private
*
* @param {module:models/room} room
*
* @return {module:client.Promise} Promise which resolves to the megolm
* sessionId when setup is complete.
*/
MegolmEncryption.prototype._prepareNewSession = function(room) {
var session_id = this._olmDevice.createOutboundGroupSession();
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
this._olmDevice.addInboundGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, session_id,
key.key
);
// we're going to share the key with all current members of the room,
// so we can reset this.
this._devicesPendingKeyShare = {};
var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
var shareMap = {};
for (var i = 0; i < roomMembers.length; i++) {
var userId = roomMembers[i];
shareMap[userId] = true;
}
var self = this;
// TODO: we need to give the user a chance to block any devices or users
// before we send them the keys; it's too late to download them here.
return this._crypto.downloadKeys(
roomMembers, false
).then(function(res) {
return self._shareKeyWithDevices(session_id, shareMap);
}).then(function() {
if (self._discardNewSession) {
// we've had cause to reset the session_id since starting this process.
// we'll use the current session for any currently pending events, but
// don't save it as the current _outboundSessionId, so that new events
// will use a new session.
console.log("Session generation complete, but discarding");
} else {
self._outboundSessionId = session_id;
}
return session_id;
}).finally(function() {
self._discardNewSession = false;
});
};
/**
* @private
*
* @param {string} session_id
* @param {Object<string, Object<string, boolean>|boolean>} shareMap
*
* @return {module:client.Promise} Promise which resolves once the key sharing
* message has been sent.
*/
MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap) {
var self = this;
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
var payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: session_id,
session_key: key.key,
chain_index: key.chain_index,
}
};
return self._crypto.ensureOlmSessionsForUsers(
utils.keys(shareMap)
).then(function(devicemap) {
var contentMap = {};
var haveTargets = false;
for (var userId in devicemap) {
if (!devicemap.hasOwnProperty(userId)) {
continue;
}
var devicesToShareWith = shareMap[userId];
var deviceInfos = devicemap[userId];
for (var deviceId in deviceInfos) {
if (!deviceInfos.hasOwnProperty(deviceId)) {
continue;
}
if (devicesToShareWith === true) {
// all devices
} else if (!devicesToShareWith[deviceId]) {
// not a new device
continue;
}
console.log(
"sharing keys with device " + userId + ":" + deviceId
);
var deviceInfo = deviceInfos[deviceId].device;
if (!contentMap[userId]) {
contentMap[userId] = {};
}
contentMap[userId][deviceId] =
olmlib.encryptMessageForDevices(
self._deviceId,
self._olmDevice,
[deviceInfo.getIdentityKey()],
payload
);
haveTargets = true;
}
}
if (!haveTargets) {
return q();
}
// TODO: retries
return self._baseApis.sendToDevice("m.room.encrypted", contentMap);
});
};
/**
* @inheritdoc
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
var self = this;
return this._ensureOutboundSession(room).then(function(session_id) {
var payloadJson = {
room_id: self._roomId,
type: eventType,
content: content
};
var ciphertext = self._olmDevice.encryptGroupMessage(
session_id, JSON.stringify(payloadJson)
);
var encryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext,
session_id: session_id,
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
device_id: self._deviceId,
};
return encryptedContent;
});
};
/**
* @inheritdoc
*
* @param {module:models/event.MatrixEvent} event event causing the change
* @param {module:models/room-member} member user whose membership changed
* @param {string=} oldMembership previous membership
*/
MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembership) {
var newMembership = member.membership;
if (newMembership === 'join') {
// new member in the room.
this._devicesPendingKeyShare[member.userId] = true;
return;
}
if (newMembership === 'invite' && oldMembership !== 'join') {
// we don't (yet) share keys with invited members, so nothing to do yet
return;
}
// otherwise we assume the user is leaving, and start a new outbound session.
if (this._outboundSessionId) {
console.log("Discarding outbound megolm session due to change in " +
"membership of " + member.userId + " (" + oldMembership +
"->" + newMembership + ")");
this._outboundSessionId = null;
}
if (this._prepPromise) {
console.log("Discarding as-yet-incomplete megolm session due to " +
"change in membership of " + member.userId + " (" +
oldMembership + "->" + newMembership + ")");
this._discardNewSession = true;
}
};
/**
* @inheritdoc
*
* @param {string} userId owner of the device
* @param {string} deviceId deviceId of the device
*/
MegolmEncryption.prototype.onNewDevice = function(userId, deviceId) {
var d = this._devicesPendingKeyShare[userId];
if (d === true) {
// we already want to share keys with all devices for this user
return;
}
if (!d) {
this._devicesPendingKeyShare[userId] = d = {};
}
d[deviceId] = true;
};
/**
* Megolm decryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.DecryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.DecryptionAlgorithm}
*/
function MegolmDecryption(params) {
base.DecryptionAlgorithm.call(this, params);
}
utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
/**
* @inheritdoc
*
* @param {object} event raw event
*
* @return {object} object with 'result' key with decrypted payload (with
* properties 'type', 'content') and a 'sessionExists' key.
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
MegolmDecryption.prototype.decryptEvent = function(event) {
var content = event.content;
console.log("decrypting " + event.event_id + " with sid " +
content.session_id);
if (!content.sender_key || !content.session_id ||
!content.ciphertext
) {
throw new base.DecryptionError("Missing fields in input");
}
try {
var res = this._olmDevice.decryptGroupMessage(
event.room_id, content.sender_key, content.session_id, content.ciphertext
);
if (res.sessionExists) {
res.result = JSON.parse(res.result);
return res;
} else {
return {sessionExists: false};
}
} catch (e) {
throw new base.DecryptionError(e);
}
};
/**
* @inheritdoc
*
* @param {module:models/event.MatrixEvent} event key event
*/
MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
console.log("Adding key from ", event);
var content = event.getContent();
if (!content.room_id ||
!content.session_id ||
!content.session_key
) {
console.error("key event is missing fields");
return;
}
this._olmDevice.addInboundGroupSession(
content.room_id, event.getSenderKey(), content.session_id,
content.session_key, event.getKeysClaimed()
);
};
base.registerAlgorithm(
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption
);
+226
View File
@@ -0,0 +1,226 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* Defines m.olm encryption/decryption
*
* @module crypto/algorithms/olm
*/
var q = require('q');
var utils = require("../../utils");
var olmlib = require("../olmlib");
var DeviceInfo = require("../deviceinfo");
var DeviceVerification = DeviceInfo.DeviceVerification;
var base = require("./base");
/**
* Olm encryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.EncryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.EncryptionAlgorithm}
*/
function OlmEncryption(params) {
base.EncryptionAlgorithm.call(this, params);
this._sessionPrepared = false;
this._prepPromise = null;
}
utils.inherits(OlmEncryption, base.EncryptionAlgorithm);
/**
* @private
* @param {string[]} roomMembers list of currently-joined users in the room
* @return {module:client.Promise} Promise which resolves when setup is complete
*/
OlmEncryption.prototype._ensureSession = function(roomMembers) {
if (this._prepPromise) {
// prep already in progress
return this._prepPromise;
}
if (this._sessionPrepared) {
// prep already done
return q();
}
var self = this;
this._prepPromise = self._crypto.downloadKeys(roomMembers, true).then(function(res) {
return self._crypto.ensureOlmSessionsForUsers(roomMembers);
}).then(function() {
self._sessionPrepared = true;
}).finally(function() {
self._prepPromise = null;
});
return this._prepPromise;
};
/**
* @inheritdoc
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
// pick the list of recipients based on the membership list.
//
// TODO: there is a race condition here! What if a new user turns up
// just as you are sending a secret message?
var users = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
var self = this;
return this._ensureSession(users).then(function() {
var participantKeys = [];
for (var i = 0; i < users.length; ++i) {
var userId = users[i];
var devices = self._crypto.getStoredDevicesForUser(userId);
for (var j = 0; j < devices.length; ++j) {
var deviceInfo = devices[j];
var key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
// don't bother setting up session to ourself
continue;
}
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
// don't bother setting up sessions with blocked users
continue;
}
participantKeys.push(key);
}
}
return olmlib.encryptMessageForDevices(
self._deviceId, self._olmDevice, participantKeys, {
room_id: room.roomId,
type: eventType,
content: content,
// Include the ED25519 key so that the recipient knows what
// device this message came from.
// We don't need to include the curve25519 key since the
// recipient will already know this from the olm headers.
// When combined with the device keys retrieved from the
// homeserver signed by the ed25519 key this proves that
// the curve25519 key and the ed25519 key are owned by
// the same device.
keys: {
"ed25519": self._olmDevice.deviceEd25519Key
},
}
);
});
};
/**
* Olm decryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.DecryptionAlgorithm}
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.DecryptionAlgorithm}
*/
function OlmDecryption(params) {
base.DecryptionAlgorithm.call(this, params);
}
utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
/**
* @inheritdoc
*
* @param {object} event raw event
*
* @return {object} result object with result property with the decrypted
* payload (with properties 'type', 'content'), and a "sessionExists" key
* always set to true.
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
OlmDecryption.prototype.decryptEvent = function(event) {
var content = event.content;
var deviceKey = content.sender_key;
var ciphertext = content.ciphertext;
if (!ciphertext) {
throw new base.DecryptionError("Missing ciphertext");
}
if (!(this._olmDevice.deviceCurve25519Key in content.ciphertext)) {
throw new base.DecryptionError("Not included in recipients");
}
var message = content.ciphertext[this._olmDevice.deviceCurve25519Key];
var sessionIds = this._olmDevice.getSessionIdsForDevice(deviceKey);
var payloadString = null;
var foundSession = false;
for (var i = 0; i < sessionIds.length; i++) {
var sessionId = sessionIds[i];
var res = this._olmDevice.decryptMessage(
deviceKey, sessionId, message.type, message.body
);
payloadString = res.payload;
if (payloadString) {
console.log("decrypted with sessionId " + sessionId);
break;
}
if (res.matchesInbound) {
// this was a prekey message which matched this session; don't
// create a new session.
foundSession = true;
break;
}
}
if (message.type === 0 && !foundSession && payloadString === null) {
try {
payloadString = this._olmDevice.createInboundSession(
deviceKey, message.type, message.body
);
console.log("created new inbound sesion");
} catch (e) {
// Failed to decrypt with a new session.
console.warn("Failed to decrypt message with new session", e);
}
}
// TODO: Check the sender user id matches the sender key.
// TODO: check the room_id and fingerprint
if (payloadString !== null) {
var payload = JSON.parse(payloadString);
return {
result: payload,
sessionExists: true,
keysProved: {curve25519: deviceKey},
keysClaimed: payload.keys || {}
};
} else {
throw new base.DecryptionError("Bad Encrypted Message");
}
};
base.registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
+145
View File
@@ -0,0 +1,145 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module crypto/deviceinfo
*/
/**
* Information about a user's device
*
* @constructor
* @alias module:crypto/deviceinfo
*
* @property {string} deviceId the ID of this device
*
* @property {string[]} algorithms list of algorithms supported by this device
*
* @property {Object.<string,string>} keys a map from
* &lt;key type&gt;:&lt;id&gt; -> &lt;base64-encoded key&gt;>
*
* @property {module:crypto/deviceinfo.DeviceVerification} verified
* whether the device has been verified by the user
*
* @property {Object} unsigned additional data from the homeserver
*
* @param {string} deviceId id of the device
*/
function DeviceInfo(deviceId) {
// you can't change the deviceId
Object.defineProperty(this, 'deviceId', {
enumerable: true,
value: deviceId,
});
this.algorithms = [];
this.keys = {};
this.verified = DeviceVerification.UNVERIFIED;
this.unsigned = {};
}
/**
* rehydrate a DeviceInfo from the session store
*
* @param {object} obj raw object from session store
* @param {string} deviceId id of the device
*
* @return {module:crypto~DeviceInfo} new DeviceInfo
*/
DeviceInfo.fromStorage = function(obj, deviceId) {
var res = new DeviceInfo(deviceId);
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
res[prop] = obj[prop];
}
}
return res;
};
/**
* Prepare a DeviceInfo for JSON serialisation in the session store
*
* @return {object} deviceinfo with non-serialised members removed
*/
DeviceInfo.prototype.toStorage = function() {
return {
algorithms: this.algorithms,
keys: this.keys,
verified: this.verified,
unsigned: this.unsigned,
};
};
/**
* Get the fingerprint for this device (ie, the Ed25519 key)
*
* @return {string} base64-encoded fingerprint of this device
*/
DeviceInfo.prototype.getFingerprint = function() {
return this.keys["ed25519:" + this.deviceId];
};
/**
* Get the identity key for this device (ie, the Curve25519 key)
*
* @return {string} base64-encoded identity key of this device
*/
DeviceInfo.prototype.getIdentityKey = function() {
return this.keys["curve25519:" + this.deviceId];
};
/**
* Get the configured display name for this device, if any
*
* @return {string?} displayname
*/
DeviceInfo.prototype.getDisplayName = function() {
return this.unsigned.device_display_name || null;
};
/**
* Returns true if this device is blocked
*
* @return {Boolean} true if blocked
*/
DeviceInfo.prototype.isBlocked = function() {
return this.verified == DeviceVerification.BLOCKED;
};
/**
* Returns true if this device is verified
*
* @return {Boolean} true if verified
*/
DeviceInfo.prototype.isVerified = function() {
return this.verified == DeviceVerification.VERIFIED;
};
/**
* @enum
*/
DeviceInfo.DeviceVerification = {
VERIFIED: 1,
UNVERIFIED: 0,
BLOCKED: -1,
};
var DeviceVerification = DeviceInfo.DeviceVerification;
/** */
module.exports = DeviceInfo;
+1110
View File
File diff suppressed because it is too large Load Diff
+78
View File
@@ -0,0 +1,78 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module olmlib
*
* Utilities common to olm encryption algorithms
*/
var utils = require("../utils");
/**
* matrix algorithm tag for olm
*/
module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
/**
* matrix algorithm tag for megolm
*/
module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
/**
* Encrypt an event payload for a list of devices
*
* @param {string} ourDeviceId
* @param {module:crypto/OlmDevice} olmDevice olm.js wrapper
* @param {string[]} participantKeys list of curve25519 keys to encrypt for
* @param {object} payloadFields fields to include in the encrypted payload
*
* @return {object} content for an m.room.encrypted event
*/
module.exports.encryptMessageForDevices = function(
ourDeviceId, olmDevice, participantKeys, payloadFields
) {
participantKeys.sort();
var participantHash = ""; // Olm.sha256(participantKeys.join());
var payloadJson = {
fingerprint: participantHash,
sender_device: ourDeviceId,
};
utils.extend(payloadJson, payloadFields);
var ciphertext = {};
var payloadString = JSON.stringify(payloadJson);
for (var i = 0; i < participantKeys.length; ++i) {
var deviceKey = participantKeys[i];
var sessionId = olmDevice.getSessionIdForDevice(deviceKey);
if (sessionId === null) {
// If we don't have a session for a device then
// we can't encrypt a message for it.
continue;
}
console.log("Using sessionid " + sessionId + " for device " + deviceKey);
ciphertext[deviceKey] = olmDevice.encryptMessage(
deviceKey, sessionId, payloadString
);
}
var encryptedContent = {
algorithm: module.exports.OLM_ALGORITHM,
sender_key: olmDevice.deviceCurve25519Key,
ciphertext: ciphertext
};
return encryptedContent;
};
+141
View File
@@ -0,0 +1,141 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module filter-component
*/
/**
* Checks if a value matches a given field value, which may be a * terminated
* wildcard pattern.
* @param {String} actual_value The value to be compared
* @param {String} filter_value The filter pattern to be compared
* @return {bool} true if the actual_value matches the filter_value
*/
function _matches_wildcard(actual_value, filter_value) {
if (filter_value.endsWith("*")) {
var type_prefix = filter_value.slice(0, -1);
return actual_value.substr(0, type_prefix.length) === type_prefix;
}
else {
return actual_value === filter_value;
}
}
/**
* FilterComponent is a section of a Filter definition which defines the
* types, rooms, senders filters etc to be applied to a particular type of resource.
* This is all ported over from synapse's Filter object.
*
* N.B. that synapse refers to these as 'Filters', and what js-sdk refers to as
* 'Filters' are referred to as 'FilterCollections'.
*
* @constructor
* @param {Object} the definition of this filter JSON, e.g. { 'contains_url': true }
*/
function FilterComponent(filter_json) {
this.filter_json = filter_json;
this.types = filter_json.types || null;
this.not_types = filter_json.not_types || [];
this.rooms = filter_json.rooms || null;
this.not_rooms = filter_json.not_rooms || [];
this.senders = filter_json.senders || null;
this.not_senders = filter_json.not_senders || [];
this.contains_url = filter_json.contains_url || null;
}
/**
* Checks with the filter component matches the given event
* @param {MatrixEvent} event event to be checked against the filter
* @return {bool} true if the event matches the filter
*/
FilterComponent.prototype.check = function(event) {
return this._checkFields(
event.getRoomId(),
event.getSender(),
event.getType(),
event.getContent() ? event.getContent().url !== undefined : false
);
};
/**
* Checks whether the filter component matches the given event fields.
* @param {String} room_id the room_id for the event being checked
* @param {String} sender the sender of the event being checked
* @param {String} event_type the type of the event being checked
* @param {String} contains_url whether the event contains a content.url field
* @return {bool} true if the event fields match the filter
*/
FilterComponent.prototype._checkFields =
function(room_id, sender, event_type, contains_url)
{
var literal_keys = {
"rooms": function(v) { return room_id === v; },
"senders": function(v) { return sender === v; },
"types": function(v) { return _matches_wildcard(event_type, v); },
};
var self = this;
Object.keys(literal_keys).forEach(function(name) {
var match_func = literal_keys[name];
var not_name = "not_" + name;
var disallowed_values = self[not_name];
if (disallowed_values.map(match_func)) {
return false;
}
var allowed_values = self[name];
if (allowed_values) {
if (!allowed_values.map(match_func)) {
return false;
}
}
});
var contains_url_filter = this.filter_json.contains_url;
if (contains_url_filter !== undefined) {
if (contains_url_filter !== contains_url) {
return false;
}
}
return true;
};
/**
* Filters a list of events down to those which match this filter component
* @param {MatrixEvent[]} events Events to be checked againt the filter component
* @return {MatrixEvent[]} events which matched the filter component
*/
FilterComponent.prototype.filter = function(events) {
return events.filter(this.check, this);
};
/**
* Returns the limit field for a given filter component, providing a default of
* 10 if none is otherwise specified. Cargo-culted from Synapse.
* @return {Number} the limit for this filter component.
*/
FilterComponent.prototype.limit = function() {
return this.filter_json.limit !== undefined ? this.filter_json.limit : 10;
};
/** The FilterComponent class */
module.exports = FilterComponent;
+92
View File
@@ -18,6 +18,8 @@ limitations under the License.
* @module filter
*/
var FilterComponent = require("./filter-component");
/**
* @param {Object} obj
* @param {string} keyNesting
@@ -49,6 +51,14 @@ function Filter(userId, filterId) {
this.definition = {};
}
/**
* Get the ID of this filter on your homeserver (if known)
* @return {?Number} The filter ID
*/
Filter.prototype.getFilterId = function() {
return this.filterId;
};
/**
* Get the JSON body of the filter.
* @return {Object} The filter definition
@@ -63,6 +73,88 @@ Filter.prototype.getDefinition = function() {
*/
Filter.prototype.setDefinition = function(definition) {
this.definition = definition;
// This is all ported from synapse's FilterCollection()
// definitions look something like:
// {
// "room": {
// "rooms": ["!abcde:example.com"],
// "not_rooms": ["!123456:example.com"],
// "state": {
// "types": ["m.room.*"],
// "not_rooms": ["!726s6s6q:example.com"],
// },
// "timeline": {
// "limit": 10,
// "types": ["m.room.message"],
// "not_rooms": ["!726s6s6q:example.com"],
// "not_senders": ["@spam:example.com"]
// "contains_url": true
// },
// "ephemeral": {
// "types": ["m.receipt", "m.typing"],
// "not_rooms": ["!726s6s6q:example.com"],
// "not_senders": ["@spam:example.com"]
// }
// },
// "presence": {
// "types": ["m.presence"],
// "not_senders": ["@alice:example.com"]
// },
// "event_format": "client",
// "event_fields": ["type", "content", "sender"]
// }
var room_filter_json = definition.room;
// consider the top level rooms/not_rooms filter
var room_filter_fields = {};
if (room_filter_json) {
if (room_filter_json.rooms) {
room_filter_fields.rooms = room_filter_json.rooms;
}
if (room_filter_json.rooms) {
room_filter_fields.not_rooms = room_filter_json.not_rooms;
}
this._include_leave = room_filter_json.include_leave || false;
}
this._room_filter = new FilterComponent(room_filter_fields);
this._room_timeline_filter = new FilterComponent(
room_filter_json ? (room_filter_json.timeline || {}) : {}
);
// don't bother porting this from synapse yet:
// this._room_state_filter =
// new FilterComponent(room_filter_json.state || {});
// this._room_ephemeral_filter =
// new FilterComponent(room_filter_json.ephemeral || {});
// this._room_account_data_filter =
// new FilterComponent(room_filter_json.account_data || {});
// this._presence_filter =
// new FilterComponent(definition.presence || {});
// this._account_data_filter =
// new FilterComponent(definition.account_data || {});
};
/**
* Get the room.timeline filter component of the filter
* @return {FilterComponent} room timeline filter component
*/
Filter.prototype.getRoomTimelineFilterComponent = function() {
return this._room_timeline_filter;
};
/**
* Filter the list of events based on whether they are allowed in a timeline
* based on this filter
* @param {MatrixEvent[]} events the list of events being filtered
* @return {MatrixEvent[]} the list of events which match the filter
*/
Filter.prototype.filterRoomTimeline = function(events) {
return this._room_timeline_filter.filter(this._room_filter.filter(events));
};
/**
+24 -9
View File
@@ -21,6 +21,11 @@ limitations under the License.
var q = require("q");
var utils = require("./utils");
// we use our own implementation of setTimeout, so that if we get suspended in
// the middle of a /sync, we cancel the sync as soon as we awake, rather than
// waiting for the delay to elapse.
var callbacks = require("./realtime-callbacks");
/*
TODO:
- CS: complete register function (doing stages)
@@ -42,6 +47,11 @@ module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable";
*/
module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
/**
* URI path for the media repo API
*/
module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
/**
* Construct a MatrixHttpApi.
* @constructor
@@ -125,12 +135,12 @@ module.exports.MatrixHttpApi.prototype = {
cb(new Error('Timeout'));
};
xhr.timeout_timer = setTimeout(timeout_fn, 30000);
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
xhr.onreadystatechange = function() {
switch (xhr.readyState) {
case global.XMLHttpRequest.DONE:
clearTimeout(xhr.timeout_timer);
callbacks.clearTimeout(xhr.timeout_timer);
var err;
if (!xhr.responseText) {
err = new Error('No response body.');
@@ -152,10 +162,10 @@ module.exports.MatrixHttpApi.prototype = {
}
};
xhr.upload.addEventListener("progress", function(ev) {
clearTimeout(xhr.timeout_timer);
callbacks.clearTimeout(xhr.timeout_timer);
upload.loaded = ev.loaded;
upload.total = ev.total;
xhr.timeout_timer = setTimeout(timeout_fn, 30000);
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
defer.notify(ev);
});
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
@@ -178,9 +188,10 @@ module.exports.MatrixHttpApi.prototype = {
upload.request = this.opts.request({
uri: url,
qs: queryParams,
method: "POST"
method: "POST",
headers: {"Content-Type": file.type},
body: file.stream
}, requestCallback(defer, callback, this.opts.onlyData));
file.stream.pipe(this.opts.request);
}
this.uploads.push(upload);
@@ -450,9 +461,13 @@ module.exports.MatrixHttpApi.prototype = {
var timeoutId;
var timedOut = false;
var req;
if (localTimeoutMs) {
timeoutId = setTimeout(function() {
timeoutId = callbacks.setTimeout(function() {
timedOut = true;
if (req && req.abort) {
req.abort();
}
defer.reject(new module.exports.MatrixError({
error: "Locally timed out waiting for a response",
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
@@ -464,7 +479,7 @@ module.exports.MatrixHttpApi.prototype = {
var reqPromise = defer.promise;
try {
var req = this.opts.request(
req = this.opts.request(
{
uri: uri,
method: method,
@@ -477,7 +492,7 @@ module.exports.MatrixHttpApi.prototype = {
},
function(err, response, body) {
if (localTimeoutMs) {
clearTimeout(timeoutId);
callbacks.clearTimeout(timeoutId);
if (timedOut) {
return; // already rejected promise
}
+4 -2
View File
@@ -30,11 +30,13 @@ module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi;
module.exports.MatrixError = require("./http-api").MatrixError;
/** The {@link module:client.MatrixClient|MatrixClient} class. */
module.exports.MatrixClient = require("./client").MatrixClient;
/** The {@link module:models/room~Room|Room} class. */
/** The {@link module:models/room|Room} class. */
module.exports.Room = require("./models/room");
/** The {@link module:models/event-timeline~EventTimeline} class. */
module.exports.EventTimeline = require("./models/event-timeline");
/** The {@link module:models/room-member~RoomMember|RoomMember} class. */
/** The {@link module:models/event-timeline-set~EventTimelineSet} class. */
module.exports.EventTimelineSet = require("./models/event-timeline-set");
/** The {@link module:models/room-member|RoomMember} class. */
module.exports.RoomMember = require("./models/room-member");
/** The {@link module:models/room-state~RoomState|RoomState} class. */
module.exports.RoomState = require("./models/room-state");
+654
View File
@@ -0,0 +1,654 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/event-timeline-set
*/
var EventEmitter = require("events").EventEmitter;
var utils = require("../utils");
var EventTimeline = require("./event-timeline");
// var DEBUG = false;
var DEBUG = true;
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console);
} else {
var debuglog = function() {};
}
/**
* Construct a set of EventTimeline objects, typically on behalf of a given
* room. A room may have multiple EventTimelineSets for different levels
* of filtering. The global notification list is also an EventTimelineSet, but
* lacks a room.
*
* <p>This is an ordered sequence of timelines, which may or may not
* be continuous. Each timeline lists a series of events, as well as tracking
* the room state at the start and the end of the timeline (if appropriate).
* It also tracks forward and backward pagination tokens, as well as containing
* links to the next timeline in the sequence.
*
* <p>There is one special timeline - the 'live' timeline, which represents the
* timeline to which events are being added in real-time as they are received
* from the /sync API. Note that you should not retain references to this
* timeline - even if it is the current timeline right now, it may not remain
* so if the server gives us a timeline gap in /sync.
*
* <p>In order that we can find events from their ids later, we also maintain a
* map from event_id to timeline and index.
*
* @constructor
* @param {?Room} room the optional room for this timelineSet
* @param {Object} opts hash of options inherited from Room.
* opts.timelineSupport gives whether timeline support is enabled
* opts.filter is the filter object, if any, for this timelineSet.
*/
function EventTimelineSet(room, opts) {
this.room = room;
this._timelineSupport = Boolean(opts.timelineSupport);
this._liveTimeline = new EventTimeline(this);
// just a list - *not* ordered.
this._timelines = [this._liveTimeline];
this._eventIdToTimeline = {};
this._filter = opts.filter || null;
}
utils.inherits(EventTimelineSet, EventEmitter);
/**
* Get the filter object this timeline set is filtered on, if any
* @return {?Filter} the optional filter for this timelineSet
*/
EventTimelineSet.prototype.getFilter = function() {
return this._filter;
};
/**
* Set the filter object this timeline set is filtered on
* (passed to the server when paginating via /messages).
* @param {Filter} filter the filter for this timelineSet
*/
EventTimelineSet.prototype.setFilter = function(filter) {
this._filter = filter;
};
/**
* Get the list of pending sent events for this timelineSet's room, filtered
* by the timelineSet's filter if appropriate.
*
* @return {module:models/event.MatrixEvent[]} A list of the sent events
* waiting for remote echo.
*
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
*/
EventTimelineSet.prototype.getPendingEvents = function() {
if (!this.room) {
return [];
}
if (this._filter) {
return this._filter.filterRoomTimeline(this.room.getPendingEvents());
}
else {
return this.room.getPendingEvents();
}
};
/**
* Get the live timeline for this room.
*
* @return {module:models/event-timeline~EventTimeline} live timeline
*/
EventTimelineSet.prototype.getLiveTimeline = function() {
return this._liveTimeline;
};
/**
* Return the timeline (if any) this event is in.
* @param {String} eventId the eventId being sought
* @return {module:models/event-timeline~EventTimeline} timeline
*/
EventTimelineSet.prototype.eventIdToTimeline = function(eventId) {
return this._eventIdToTimeline[eventId];
};
/**
* Track a new event as if it were in the same timeline as an old event,
* replacing it.
* @param {String} oldEventId event ID of the original event
* @param {String} newEventId event ID of the replacement event
*/
EventTimelineSet.prototype.replaceEventId = function(oldEventId, newEventId) {
var existingTimeline = this._eventIdToTimeline[oldEventId];
if (existingTimeline) {
delete this._eventIdToTimeline[oldEventId];
this._eventIdToTimeline[newEventId] = existingTimeline;
}
};
/**
* Reset the live timeline, and start a new one.
*
* <p>This is used when /sync returns a 'limited' timeline.
*
* @param {string=} backPaginationToken token for back-paginating the new timeline
* @param {?bool} flush Whether to flush the non-live timelines too.
*
* @fires module:client~MatrixClient#event:"Room.timelineReset"
*/
EventTimelineSet.prototype.resetLiveTimeline = function(backPaginationToken, flush) {
var newTimeline;
if (!this._timelineSupport || flush) {
// if timeline support is disabled, forget about the old timelines
newTimeline = new EventTimeline(this);
this._timelines = [newTimeline];
this._eventIdToTimeline = {};
} else {
newTimeline = this.addTimeline();
}
// initialise the state in the new timeline from our last known state
var evMap = this._liveTimeline.getState(EventTimeline.FORWARDS).events;
var events = [];
for (var evtype in evMap) {
if (!evMap.hasOwnProperty(evtype)) { continue; }
for (var stateKey in evMap[evtype]) {
if (!evMap[evtype].hasOwnProperty(stateKey)) { continue; }
events.push(evMap[evtype][stateKey]);
}
}
newTimeline.initialiseState(events);
// make sure we set the pagination token before firing timelineReset,
// otherwise clients which start back-paginating will fail, and then get
// stuck without realising that they *can* back-paginate.
newTimeline.setPaginationToken(backPaginationToken, EventTimeline.BACKWARDS);
this._liveTimeline = newTimeline;
this.emit("Room.timelineReset", this.room, this);
};
/**
* Get the timeline which contains the given event, if any
*
* @param {string} eventId event ID to look for
* @return {?module:models/event-timeline~EventTimeline} timeline containing
* the given event, or null if unknown
*/
EventTimelineSet.prototype.getTimelineForEvent = function(eventId) {
var res = this._eventIdToTimeline[eventId];
return (res === undefined) ? null : res;
};
/**
* Get an event which is stored in our timelines
*
* @param {string} eventId event ID to look for
* @return {?module:models/event~MatrixEvent} the given event, or undefined if unknown
*/
EventTimelineSet.prototype.findEventById = function(eventId) {
var tl = this.getTimelineForEvent(eventId);
if (!tl) {
return undefined;
}
return utils.findElement(tl.getEvents(),
function(ev) { return ev.getId() == eventId; });
};
/**
* Add a new timeline to this timeline list
*
* @return {module:models/event-timeline~EventTimeline} newly-created timeline
*/
EventTimelineSet.prototype.addTimeline = function() {
if (!this._timelineSupport) {
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
" parameter to true when creating MatrixClient to enable" +
" it.");
}
var timeline = new EventTimeline(this);
this._timelines.push(timeline);
return timeline;
};
/**
* Add events to a timeline
*
* <p>Will fire "Room.timeline" for each event added.
*
* @param {MatrixEvent[]} events A list of events to add.
*
* @param {boolean} toStartOfTimeline True to add these events to the start
* (oldest) instead of the end (newest) of the timeline. If true, the oldest
* event will be the <b>last</b> element of 'events'.
*
* @param {module:models/event-timeline~EventTimeline} timeline timeline to
* add events to.
*
* @param {string=} paginationToken token for the next batch of events
*
* @fires module:client~MatrixClient#event:"Room.timeline"
*
*/
EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
timeline, paginationToken) {
if (!timeline) {
throw new Error(
"'timeline' not specified for EventTimelineSet.addEventsToTimeline"
);
}
if (!toStartOfTimeline && timeline == this._liveTimeline) {
throw new Error(
"EventTimelineSet.addEventsToTimeline cannot be used for adding events to " +
"the live timeline - use Room.addLiveEvents instead"
);
}
if (this._filter) {
events = this._filter.filterRoomTimeline(events);
if (!events.length) {
return;
}
}
var direction = toStartOfTimeline ? EventTimeline.BACKWARDS :
EventTimeline.FORWARDS;
var inverseDirection = toStartOfTimeline ? EventTimeline.FORWARDS :
EventTimeline.BACKWARDS;
// Adding events to timelines can be quite complicated. The following
// illustrates some of the corner-cases.
//
// Let's say we start by knowing about four timelines. timeline3 and
// timeline4 are neighbours:
//
// timeline1 timeline2 timeline3 timeline4
// [M] [P] [S] <------> [T]
//
// Now we paginate timeline1, and get the following events from the server:
// [M, N, P, R, S, T, U].
//
// 1. First, we ignore event M, since we already know about it.
//
// 2. Next, we append N to timeline 1.
//
// 3. Next, we don't add event P, since we already know about it,
// but we do link together the timelines. We now have:
//
// timeline1 timeline2 timeline3 timeline4
// [M, N] <---> [P] [S] <------> [T]
//
// 4. Now we add event R to timeline2:
//
// timeline1 timeline2 timeline3 timeline4
// [M, N] <---> [P, R] [S] <------> [T]
//
// Note that we have switched the timeline we are working on from
// timeline1 to timeline2.
//
// 5. We ignore event S, but again join the timelines:
//
// timeline1 timeline2 timeline3 timeline4
// [M, N] <---> [P, R] <---> [S] <------> [T]
//
// 6. We ignore event T, and the timelines are already joined, so there
// is nothing to do.
//
// 7. Finally, we add event U to timeline4:
//
// timeline1 timeline2 timeline3 timeline4
// [M, N] <---> [P, R] <---> [S] <------> [T, U]
//
// The important thing to note in the above is what happened when we
// already knew about a given event:
//
// - if it was appropriate, we joined up the timelines (steps 3, 5).
// - in any case, we started adding further events to the timeline which
// contained the event we knew about (steps 3, 5, 6).
//
//
// So much for adding events to the timeline. But what do we want to do
// with the pagination token?
//
// In the case above, we will be given a pagination token which tells us how to
// get events beyond 'U' - in this case, it makes sense to store this
// against timeline4. But what if timeline4 already had 'U' and beyond? in
// that case, our best bet is to throw away the pagination token we were
// given and stick with whatever token timeline4 had previously. In short,
// we want to only store the pagination token if the last event we receive
// is one we didn't previously know about.
//
// We make an exception for this if it turns out that we already knew about
// *all* of the events, and we weren't able to join up any timelines. When
// that happens, it means our existing pagination token is faulty, since it
// is only telling us what we already know. Rather than repeatedly
// paginating with the same token, we might as well use the new pagination
// token in the hope that we eventually work our way out of the mess.
var didUpdate = false;
var lastEventWasNew = false;
for (var i = 0; i < events.length; i++) {
var event = events[i];
var eventId = event.getId();
var existingTimeline = this._eventIdToTimeline[eventId];
if (!existingTimeline) {
// we don't know about this event yet. Just add it to the timeline.
this.addEventToTimeline(event, timeline, toStartOfTimeline);
lastEventWasNew = true;
didUpdate = true;
continue;
}
lastEventWasNew = false;
if (existingTimeline == timeline) {
debuglog("Event " + eventId + " already in timeline " + timeline);
continue;
}
var neighbour = timeline.getNeighbouringTimeline(direction);
if (neighbour) {
// this timeline already has a neighbour in the relevant direction;
// let's assume the timelines are already correctly linked up, and
// skip over to it.
//
// there's probably some edge-case here where we end up with an
// event which is in a timeline a way down the chain, and there is
// a break in the chain somewhere. But I can't really imagine how
// that would happen, so I'm going to ignore it for now.
//
if (existingTimeline == neighbour) {
debuglog("Event " + eventId + " in neighbouring timeline - " +
"switching to " + existingTimeline);
} else {
debuglog("Event " + eventId + " already in a different " +
"timeline " + existingTimeline);
}
timeline = existingTimeline;
continue;
}
// time to join the timelines.
console.info("Already have timeline for " + eventId +
" - joining timeline " + timeline + " to " +
existingTimeline);
timeline.setNeighbouringTimeline(existingTimeline, direction);
existingTimeline.setNeighbouringTimeline(timeline, inverseDirection);
timeline = existingTimeline;
didUpdate = true;
}
// see above - if the last event was new to us, or if we didn't find any
// new information, we update the pagination token for whatever
// timeline we ended up on.
if (lastEventWasNew || !didUpdate) {
timeline.setPaginationToken(paginationToken, direction);
}
};
/**
* Add an event to the end of this live timeline.
*
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
*/
EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
if (this._filter) {
var events = this._filter.filterRoomTimeline([event]);
if (!events.length) {
return;
}
}
var timeline = this._eventIdToTimeline[event.getId()];
if (timeline) {
if (duplicateStrategy === "replace") {
debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " +
event.getId());
var tlEvents = timeline.getEvents();
for (var j = 0; j < tlEvents.length; j++) {
if (tlEvents[j].getId() === event.getId()) {
// still need to set the right metadata on this event
EventTimeline.setEventMetadata(
event,
timeline.getState(EventTimeline.FORWARDS),
false
);
if (!tlEvents[j].encryptedType) {
tlEvents[j] = event;
}
// XXX: we need to fire an event when this happens.
break;
}
}
} else {
debuglog("EventTimelineSet.addLiveEvent: ignoring duplicate event " +
event.getId());
}
return;
}
this.addEventToTimeline(event, this._liveTimeline, false);
};
/**
* Add event to the given timeline, and emit Room.timeline. Assumes
* we have already checked we don't know about this event.
*
* Will fire "Room.timeline" for each event added.
*
* @param {MatrixEvent} event
* @param {EventTimeline} timeline
* @param {boolean} toStartOfTimeline
*
* @fires module:client~MatrixClient#event:"Room.timeline"
*/
EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
toStartOfTimeline) {
var eventId = event.getId();
timeline.addEvent(event, toStartOfTimeline);
this._eventIdToTimeline[eventId] = timeline;
var data = {
timeline: timeline,
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
};
this.emit("Room.timeline", event, this.room,
Boolean(toStartOfTimeline), false, data);
};
/**
* Replaces event with ID oldEventId with one with newEventId, if oldEventId is
* recognised. Otherwise, add to the live timeline. Used to handle remote echos.
*
* @param {MatrixEvent} localEvent the new event to be added to the timeline
* @param {String} oldEventId the ID of the original event
* @param {boolean} newEventId the ID of the replacement event
*
* @fires module:client~MatrixClient#event:"Room.timeline"
*/
EventTimelineSet.prototype.handleRemoteEcho = function(localEvent, oldEventId,
newEventId) {
// XXX: why don't we infer newEventId from localEvent?
var existingTimeline = this._eventIdToTimeline[oldEventId];
if (existingTimeline) {
delete this._eventIdToTimeline[oldEventId];
this._eventIdToTimeline[newEventId] = existingTimeline;
} else {
if (this._filter) {
if (this._filter.filterRoomTimeline([localEvent]).length) {
this.addEventToTimeline(localEvent, this._liveTimeline, false);
}
}
else {
this.addEventToTimeline(localEvent, this._liveTimeline, false);
}
}
};
/**
* Removes a single event from this room.
*
* @param {String} eventId The id of the event to remove
*
* @return {?MatrixEvent} the removed event, or null if the event was not found
* in this room.
*/
EventTimelineSet.prototype.removeEvent = function(eventId) {
var timeline = this._eventIdToTimeline[eventId];
if (!timeline) {
return null;
}
var removed = timeline.removeEvent(eventId);
if (removed) {
delete this._eventIdToTimeline[eventId];
var data = {
timeline: timeline,
};
this.emit("Room.timeline", removed, this.room, undefined, true, data);
}
return removed;
};
/**
* Determine where two events appear in the timeline relative to one another
*
* @param {string} eventId1 The id of the first event
* @param {string} eventId2 The id of the second event
* @return {?number} a number less than zero if eventId1 precedes eventId2, and
* greater than zero if eventId1 succeeds eventId2. zero if they are the
* same event; null if we can't tell (either because we don't know about one
* of the events, or because they are in separate timelines which don't join
* up).
*/
EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
if (eventId1 == eventId2) {
// optimise this case
return 0;
}
var timeline1 = this._eventIdToTimeline[eventId1];
var timeline2 = this._eventIdToTimeline[eventId2];
if (timeline1 === undefined) {
return null;
}
if (timeline2 === undefined) {
return null;
}
if (timeline1 === timeline2) {
// both events are in the same timeline - figure out their
// relative indices
var idx1, idx2;
var events = timeline1.getEvents();
for (var idx = 0; idx < events.length &&
(idx1 === undefined || idx2 === undefined); idx++) {
var evId = events[idx].getId();
if (evId == eventId1) {
idx1 = idx;
}
if (evId == eventId2) {
idx2 = idx;
}
}
return idx1 - idx2;
}
// the events are in different timelines. Iterate through the
// linkedlist to see which comes first.
// first work forwards from timeline1
var tl = timeline1;
while (tl) {
if (tl === timeline2) {
// timeline1 is before timeline2
return -1;
}
tl = tl.getNeighbouringTimeline(EventTimeline.FORWARDS);
}
// now try backwards from timeline1
tl = timeline1;
while (tl) {
if (tl === timeline2) {
// timeline2 is before timeline1
return 1;
}
tl = tl.getNeighbouringTimeline(EventTimeline.BACKWARDS);
}
// the timelines are not contiguous.
return null;
};
/**
* The EventTimelineSet class.
*/
module.exports = EventTimelineSet;
/**
* Fires whenever the timeline in a room is updated.
* @event module:client~MatrixClient#"Room.timeline"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {?Room} room The room, if any, whose timeline was updated.
* @param {boolean} toStartOfTimeline True if this event was added to the start
* @param {boolean} removed True if this event has just been removed from the timeline
* (beginning; oldest) of the timeline e.g. due to pagination.
*
* @param {object} data more data about the event
*
* @param {module:event-timeline.EventTimeline} data.timeline the timeline the
* event was added to/removed from
*
* @param {boolean} data.liveEvent true if the event was a real-time event
* added to the end of the live timeline
*
* @example
* matrixClient.on("Room.timeline",
* function(event, room, toStartOfTimeline, removed, data) {
* if (!toStartOfTimeline && data.liveEvent) {
* var messageToAppend = room.timeline.[room.timeline.length - 1];
* }
* });
*/
/**
* Fires whenever the live timeline in a room is reset.
*
* When we get a 'limited' sync (for example, after a network outage), we reset
* the live timeline to be empty before adding the recent events to the new
* timeline. This event is fired after the timeline is reset, and before the
* new events are added.
*
* @event module:client~MatrixClient#"Room.timelineReset"
* @param {Room} room The room whose live timeline was reset, if any
* @param {EventTimelineSet} timelineSet timelineSet room whose live timeline was reset
*/
+64 -23
View File
@@ -25,16 +25,17 @@ var MatrixEvent = require("./event").MatrixEvent;
* <p>Once a timeline joins up with its neighbour, they are linked together into a
* doubly-linked list.
*
* @param {string} roomId the ID of the room where this timeline came from
* @param {EventTimelineSet} eventTimelineSet the set of timelines this is part of
* @constructor
*/
function EventTimeline(roomId) {
this._roomId = roomId;
function EventTimeline(eventTimelineSet) {
this._eventTimelineSet = eventTimelineSet;
this._roomId = eventTimelineSet.room ? eventTimelineSet.room.roomId : null;
this._events = [];
this._baseIndex = 0;
this._startState = new RoomState(roomId);
this._startState = new RoomState(this._roomId);
this._startState.paginationToken = null;
this._endState = new RoomState(roomId);
this._endState = new RoomState(this._roomId);
this._endState.paginationToken = null;
this._prevTimeline = null;
@@ -42,6 +43,8 @@ function EventTimeline(roomId) {
// this is used by client.js
this._paginationRequests = {'b': null, 'f': null};
this._name = this._roomId + ":" + new Date().toISOString();
}
/**
@@ -89,6 +92,22 @@ EventTimeline.prototype.getRoomId = function() {
return this._roomId;
};
/**
* Get the filter for this timeline's timelineSet (if any)
* @return {Filter} filter
*/
EventTimeline.prototype.getFilter = function() {
return this._eventTimelineSet.getFilter();
};
/**
* Get the timelineSet for this timeline
* @return {EventTimelineSet} timelineSet
*/
EventTimeline.prototype.getTimelineSet = function() {
return this._eventTimelineSet;
};
/**
* Get the base index.
*
@@ -215,23 +234,29 @@ EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction)
EventTimeline.prototype.addEvent = function(event, atStart) {
var stateContext = atStart ? this._startState : this._endState;
setEventMetadata(event, stateContext, atStart);
// only call setEventMetadata on the unfiltered timelineSets
var timelineSet = this.getTimelineSet();
if (timelineSet.room &&
timelineSet.room.getUnfilteredTimelineSet() === timelineSet)
{
EventTimeline.setEventMetadata(event, stateContext, atStart);
// modify state
if (event.isState()) {
stateContext.setStateEvents([event]);
// it is possible that the act of setting the state event means we
// can set more metadata (specifically sender/target props), so try
// it again if the prop wasn't previously set. It may also mean that
// the sender/target is updated (if the event set was a room member event)
// so we want to use the *updated* member (new avatar/name) instead.
//
// However, we do NOT want to do this on member events if we're going
// back in time, else we'll set the .sender value for BEFORE the given
// member event, whereas we want to set the .sender value for the ACTUAL
// member event itself.
if (!event.sender || (event.getType() === "m.room.member" && !atStart)) {
setEventMetadata(event, stateContext, atStart);
// modify state
if (event.isState()) {
stateContext.setStateEvents([event]);
// it is possible that the act of setting the state event means we
// can set more metadata (specifically sender/target props), so try
// it again if the prop wasn't previously set. It may also mean that
// the sender/target is updated (if the event set was a room member event)
// so we want to use the *updated* member (new avatar/name) instead.
//
// However, we do NOT want to do this on member events if we're going
// back in time, else we'll set the .sender value for BEFORE the given
// member event, whereas we want to set the .sender value for the ACTUAL
// member event itself.
if (!event.sender || (event.getType() === "m.room.member" && !atStart)) {
EventTimeline.setEventMetadata(event, stateContext, atStart);
}
}
}
@@ -249,7 +274,14 @@ EventTimeline.prototype.addEvent = function(event, atStart) {
}
};
function setEventMetadata(event, stateContext, toStartOfTimeline) {
/**
* Static helper method to set sender and target properties
*
* @param {MatrixEvent} event the event whose metadata is to be set
* @param {RoomState} stateContext the room state to be queried
* @param {bool} toStartOfTimeline if true the event's forwardLooking flag is set false
*/
EventTimeline.setEventMetadata = function(event, stateContext, toStartOfTimeline) {
// set sender and target properties
event.sender = stateContext.getSentinelMember(
event.getSender()
@@ -268,7 +300,7 @@ function setEventMetadata(event, stateContext, toStartOfTimeline) {
event.forwardLooking = false;
}
}
}
};
/**
* Remove an event from the timeline
@@ -290,6 +322,15 @@ EventTimeline.prototype.removeEvent = function(eventId) {
return null;
};
/**
* Return a string to identify this timeline, for debugging
*
* @return {string} name for this timeline
*/
EventTimeline.prototype.toString = function() {
return this._name;
};
/**
* The EventTimeline class
+132 -21
View File
@@ -29,6 +29,10 @@ limitations under the License.
module.exports.EventStatus = {
/** The event was not sent and will no longer be retried. */
NOT_SENT: "not_sent",
/** The message is being encrypted */
ENCRYPTING: "encrypting",
/** The event is in the process of being sent. */
SENDING: "sending",
/** The event is in a queue waiting to be sent. */
@@ -44,12 +48,17 @@ module.exports.EventStatus = {
/**
* Construct a Matrix Event object
* @constructor
*
* @param {Object} event The raw event to be wrapped in this DAO
* @param {boolean} encrypted Was the event encrypted
* @prop {Object} event The raw event. <b>Do not access this property</b>
* directly unless you absolutely have to. Prefer the getter methods defined on
* this class. Using the getter methods shields your app from
* changes to event JSON between Matrix versions.
*
* @param {Object=} clearEvent For encrypted events, the plaintext payload for
* the event (typically containing <tt>type</tt> and <tt>content</tt> fields).
*
* @prop {Object} event The raw (possibly encrypted) event. <b>Do not access
* this property</b> directly unless you absolutely have to. Prefer the getter
* methods defined on this class. Using the getter methods shields your app
* from changes to event JSON between Matrix versions.
*
* @prop {RoomMember} sender The room member who sent this event, or null e.g.
* this is a presence event.
* @prop {RoomMember} target The room member who is the target of this event, e.g.
@@ -59,14 +68,17 @@ module.exports.EventStatus = {
* that getDirectionalContent() will return event.content and not event.prev_content.
* Default: true. <strong>This property is experimental and may change.</strong>
*/
module.exports.MatrixEvent = function MatrixEvent(event, encrypted) {
module.exports.MatrixEvent = function MatrixEvent(event, clearEvent) {
this.event = event || {};
this.sender = null;
this.target = null;
this.status = null;
this.forwardLooking = true;
this.encrypted = Boolean(encrypted);
this._clearEvent = clearEvent || {};
this._pushActions = null;
};
module.exports.MatrixEvent.prototype = {
/**
@@ -87,19 +99,22 @@ module.exports.MatrixEvent.prototype = {
},
/**
* Get the type of event.
* Get the (decrypted, if necessary) type of event.
*
* @return {string} The event type, e.g. <code>m.room.message</code>
*/
getType: function() {
return this.event.type;
return this._clearEvent.type || this.event.type;
},
/**
* Get the type of the event that will be sent to the homeserver.
* Get the (possibly encrypted) type of the event that will be sent to the
* homeserver.
*
* @return {string} The event type.
*/
getWireType: function() {
return this.encryptedType || this.event.type;
return this.event.type;
},
/**
@@ -121,19 +136,22 @@ module.exports.MatrixEvent.prototype = {
},
/**
* Get the event content JSON.
* Get the (decrypted, if necessary) event content JSON.
*
* @return {Object} The event content JSON, or an empty object.
*/
getContent: function() {
return this.event.content || {};
return this._clearEvent.content || this.event.content || {};
},
/**
* Get the event content JSON that will be sent to the homeserver.
* Get the (possibly encrypted) event content JSON that will be sent to the
* homeserver.
*
* @return {Object} The event content JSON, or an empty object.
*/
getWireContent: function() {
return this.encryptedContent || this.event.content || {};
return this.event.content || {};
},
/**
@@ -186,12 +204,74 @@ module.exports.MatrixEvent.prototype = {
return this.event.state_key !== undefined;
},
/**
* Replace the content of this event with encrypted versions.
* (This is used when sending an event; it should not be used by applications).
*
* @internal
*
* @param {string} crypto_type type of the encrypted event - typically
* <tt>"m.room.encrypted"</tt>
*
* @param {object} crypto_content raw 'content' for the encrypted event.
* @param {object} keys The local keys claimed and proved by this event.
*/
makeEncrypted: function(crypto_type, crypto_content, keys) {
// keep the plain-text data for 'view source'
this._clearEvent = {
type: this.event.type,
content: this.event.content,
keysProved: keys,
keysClaimed: keys,
};
this.event.type = crypto_type;
this.event.content = crypto_content;
},
/**
* Check if the event is encrypted.
* @return {boolean} True if this event is encrypted.
*/
isEncrypted: function() {
return this.encrypted;
return Boolean(this._clearEvent.type);
},
/**
* The curve25519 key that sent this event
* @return {string}
*/
getSenderKey: function() {
return this.getKeysProved().curve25519 || null;
},
/**
* The keys that must have been owned by the sender of this encrypted event.
* 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 prove keys directly, but instead
* inherit a proof from the olm message that established the session.
* @return {object}
*/
getKeysProved: function() {
// The keysProved property usually isn't actually part of the decrypted
// plaintext. Instead it is added after decryption by the crypto
// algorithm in lib/crypto/algorithms.
return this._clearEvent.keysProved || {};
},
/**
* The additional keys the sender of this encrypted event claims to possess
* These don't necessarily have to come from this event itself, but may be
* implied by the cryptographic session.
* For example megolm messages don't claim keys directly, but instead
* inherit a claim from the olm message that established the session.
* @return {object}
*/
getKeysClaimed: function() {
// The keysClaimed property usually isn't actually part of the
// decrypted plaintext. Instead it is added after decryption by the
// crypto algorithm in lib/crypto/algorithms.
return this._clearEvent.keysClaimed || {};
},
getUnsigned: function() {
@@ -202,13 +282,25 @@ module.exports.MatrixEvent.prototype = {
* Update the content of an event in the same way it would be by the server
* if it were redacted before it was sent to us
*
* @param {Object} the raw event causing the redaction
* @param {module:models/event.MatrixEvent} redaction_event
* event causing the redaction
*/
makeRedacted: function(redaction_event) {
// quick sanity-check
if (!redaction_event.event) {
throw new Error("invalid redaction_event in makeRedacted");
}
// we attempt to replicate what we would see from the server if
// the event had been redacted before we saw it.
//
// The server removes (most of) the content of the event, and adds a
// "redacted_because" key to the unsigned section containing the
// redacted event.
if (!this.event.unsigned) {
this.event.unsigned = {};
}
this.event.unsigned.redacted_because = redaction_event;
this.event.unsigned.redacted_because = redaction_event.event;
var key;
for (key in this.event) {
@@ -219,10 +311,11 @@ module.exports.MatrixEvent.prototype = {
}
var keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {};
for (key in this.event.content) {
if (!this.event.content.hasOwnProperty(key)) { continue; }
var content = this.getContent();
for (key in content) {
if (!content.hasOwnProperty(key)) { continue; }
if (!keeps[key]) {
delete this.event.content[key];
delete content[key];
}
}
},
@@ -235,6 +328,24 @@ module.exports.MatrixEvent.prototype = {
isRedacted: function() {
return Boolean(this.getUnsigned().redacted_because);
},
/**
* Get the push actions, if known, for this event
*
* @return {?Object} push actions
*/
getPushActions: function() {
return this._pushActions;
},
/**
* Set the push actions for this event.
*
* @param {Object} pushActions push actions
*/
setPushActions: function(pushActions) {
this._pushActions = pushActions;
},
};
+18 -23
View File
@@ -24,7 +24,10 @@ var utils = require("../utils");
/**
* Construct a new room member.
*
* @constructor
* @alias module:models/room-member
*
* @param {string} roomId The room ID of the member.
* @param {string} userId The user ID of the member.
* @prop {string} roomId The room ID for this member.
@@ -77,11 +80,11 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) {
this.name = calculateDisplayName(this, event, roomState);
if (oldMembership !== this.membership) {
this._updateModifiedTime();
this.emit("RoomMember.membership", event, this);
this.emit("RoomMember.membership", event, this, oldMembership);
}
if (oldName !== this.name) {
this._updateModifiedTime();
this.emit("RoomMember.name", event, this);
this.emit("RoomMember.name", event, this, oldName);
}
};
@@ -102,11 +105,14 @@ RoomMember.prototype.setPowerLevelEvent = function(powerLevelEvent) {
});
var oldPowerLevel = this.powerLevel;
var oldPowerLevelNorm = this.powerLevelNorm;
this.powerLevel = (
powerLevelEvent.getContent().users[this.userId] ||
powerLevelEvent.getContent().users_default ||
0
);
if (powerLevelEvent.getContent().users[this.userId] !== undefined) {
this.powerLevel = powerLevelEvent.getContent().users[this.userId];
} else if (powerLevelEvent.getContent().users_default !== undefined) {
this.powerLevel = powerLevelEvent.getContent().users_default;
} else {
this.powerLevel = 0;
}
this.powerLevelNorm = 0;
if (maxLevel > 0) {
this.powerLevelNorm = (this.powerLevel * 100) / maxLevel;
@@ -206,21 +212,6 @@ function calculateDisplayName(member, event, roomState) {
var displayName = event.getDirectionalContent().displayname;
var selfUserId = member.userId;
/*
// FIXME: this would be great but still needs to use the
// full userId to disambiguate if needed...
if (!displayName) {
var matches = selfUserId.match(/^@(.*?):/);
if (matches) {
return matches[1];
}
else {
return selfUserId;
}
}
*/
if (!displayName) {
return selfUserId;
}
@@ -249,6 +240,8 @@ module.exports = RoomMember;
* @event module:client~MatrixClient#"RoomMember.name"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomMember} member The member whose RoomMember.name changed.
* @param {string?} oldName The previous name. Null if the member didn't have a
* name previously.
* @example
* matrixClient.on("RoomMember.name", function(event, member){
* var newName = member.name;
@@ -260,8 +253,10 @@ module.exports = RoomMember;
* @event module:client~MatrixClient#"RoomMember.membership"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomMember} member The member whose RoomMember.membership changed.
* @param {string?} oldMembership The previous membership state. Null if it's a
* new member.
* @example
* matrixClient.on("RoomMember.membership", function(event, member){
* matrixClient.on("RoomMember.membership", function(event, member, oldMembership){
* var newState = member.membership;
* });
*/
+2 -1
View File
@@ -25,7 +25,8 @@ var RoomMember = require("./room-member");
/**
* Construct room state.
* @constructor
* @param {string} roomId Required. The ID of the room which has this state.
* @param {?string} roomId Optional. The ID of the room which has this state.
* If none is specified it just tracks paginationTokens, useful for notifTimelineSet
* @prop {Object.<string, RoomMember>} members The room member dictionary, keyed
* on the user's ID.
* @prop {Object.<string, Object.<string, MatrixEvent>>} events The state
+325 -518
View File
File diff suppressed because it is too large Load Diff
+77 -8
View File
@@ -30,7 +30,13 @@ limitations under the License.
* @prop {string} displayName The 'displayname' of the user if known.
* @prop {string} avatarUrl The 'avatar_url' of the user if known.
* @prop {string} presence The presence enum if known.
* @prop {Number} lastActiveAgo The last time the user performed some action in ms.
* @prop {string} presenceStatusMsg The presence status message if known.
* @prop {Number} lastActiveAgo The time elapsed in ms since the user interacted
* proactively with the server, or we saw a message from the user
* @prop {Number} lastPresenceTs Timestamp (ms since the epoch) for when we last
* received presence data for this user. We can subtract
* lastActiveAgo from this to approximate an absolute value for
* when a user was last active.
* @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be
* an approximation and that the user should be seen as active 'now'
* @prop {Object} events The events describing this user.
@@ -39,9 +45,12 @@ limitations under the License.
function User(userId) {
this.userId = userId;
this.presence = "offline";
this.presenceStatusMsg = null;
this.displayName = userId;
this.rawDisplayName = userId;
this.avatarUrl = null;
this.lastActiveAgo = 0;
this.lastPresenceTs = 0;
this.currentlyActive = false;
this.events = {
presence: null,
@@ -71,22 +80,39 @@ User.prototype.setPresenceEvent = function(event) {
if (event.getContent().presence !== this.presence || firstFire) {
eventsToFire.push("User.presence");
}
if (event.getContent().avatar_url !== this.avatarUrl) {
if (event.getContent().avatar_url &&
event.getContent().avatar_url !== this.avatarUrl)
{
eventsToFire.push("User.avatarUrl");
}
if (event.getContent().displayname !== this.displayName) {
if (event.getContent().displayname &&
event.getContent().displayname !== this.displayName)
{
eventsToFire.push("User.displayName");
}
if (event.getContent().currently_active !== undefined &&
event.getContent().currently_active !== this.currentlyActive)
{
eventsToFire.push("User.currentlyActive");
}
this.presence = event.getContent().presence;
this.displayName = event.getContent().displayname;
this.avatarUrl = event.getContent().avatar_url;
eventsToFire.push("User.lastPresenceTs");
if (event.getContent().status_msg) {
this.presenceStatusMsg = event.getContent().status_msg;
}
if (event.getContent().displayname) {
this.displayName = event.getContent().displayname;
}
if (event.getContent().avatar_url) {
this.avatarUrl = event.getContent().avatar_url;
}
this.lastActiveAgo = event.getContent().last_active_ago;
this.lastPresenceTs = Date.now();
this.currentlyActive = event.getContent().currently_active;
if (eventsToFire.length > 0) {
this._updateModifiedTime();
}
this._updateModifiedTime();
for (var i = 0; i < eventsToFire.length; i++) {
this.emit(eventsToFire[i], event, this);
@@ -106,6 +132,17 @@ User.prototype.setDisplayName = function(name) {
}
};
/**
* Manually set this user's non-disambiguated display name. No event is emitted
* in response to this as there is no underlying MatrixEvent to emit with.
* @param {string} name The new display name.
*/
User.prototype.setRawDisplayName = function(name) {
this.rawDisplayName = name;
};
/**
* Manually set this user's avatar URL. No event is emitted in response to this
* as there is no underlying MatrixEvent to emit with.
@@ -136,11 +173,32 @@ User.prototype.getLastModifiedTime = function() {
return this._modified;
};
/**
* Get the absolute timestamp when this User was last known active on the server.
* It is *NOT* accurate if this.currentlyActive is true.
* @return {number} The timestamp
*/
User.prototype.getLastActiveTs = function() {
return this.lastPresenceTs - this.lastActiveAgo;
};
/**
* The User class.
*/
module.exports = User;
/**
* Fires whenever any user's lastPresenceTs changes,
* ie. whenever any presence event is received for a user.
* @event module:client~MatrixClient#"User.lastPresenceTs"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {User} user The user whose User.lastPresenceTs changed.
* @example
* matrixClient.on("User.lastPresenceTs", function(event, user){
* var newlastPresenceTs = user.lastPresenceTs;
* });
*/
/**
* Fires whenever any user's presence changes.
* @event module:client~MatrixClient#"User.presence"
@@ -152,6 +210,17 @@ module.exports = User;
* });
*/
/**
* Fires whenever any user's currentlyActive changes.
* @event module:client~MatrixClient#"User.currentlyActive"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {User} user The user whose User.currentlyActive changed.
* @example
* matrixClient.on("User.currentlyActive", function(event, user){
* var newCurrentlyActive = user.currentlyActive;
* });
*/
/**
* Fires whenever any user's display name changes.
* @event module:client~MatrixClient#"User.displayName"
+52 -21
View File
@@ -122,7 +122,7 @@ function PushProcessor(client) {
var eventFulfillsRoomMemberCountCondition = function(cond, ev) {
if (!cond.is) { return false; }
var room = client.getRoom(ev.room_id);
var room = client.getRoom(ev.getRoomId());
if (!room || !room.currentState || !room.currentState.members) { return false; }
var memberCount = Object.keys(room.currentState.members).filter(function(m) {
@@ -152,11 +152,12 @@ function PushProcessor(client) {
};
var eventFulfillsDisplayNameCondition = function(cond, ev) {
if (!ev.content || ! ev.content.body || typeof ev.content.body != 'string') {
var content = ev.getContent();
if (!content || !content.body || typeof content.body != 'string') {
return false;
}
var room = client.getRoom(ev.room_id);
var room = client.getRoom(ev.getRoomId());
if (!room || !room.currentState || !room.currentState.members ||
!room.currentState.getMember(client.credentials.userId)) { return false; }
@@ -165,7 +166,7 @@ function PushProcessor(client) {
// N.B. we can't use \b as it chokes on unicode. however \W seems to be okay
// as shorthand for [^0-9A-Za-z_].
var pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i');
return ev.content.body.search(pat) > -1;
return content.body.search(pat) > -1;
};
var eventFulfillsDeviceCondition = function(cond, ev) {
@@ -204,7 +205,21 @@ function PushProcessor(client) {
var valueForDottedKey = function(key, ev) {
var parts = key.split('.');
var val = ev;
var val;
// special-case the first component to deal with encrypted messages
var firstPart = parts[0];
if (firstPart == 'content') {
val = ev.getContent();
parts.shift();
} else if (firstPart == 'type') {
val = ev.getType();
parts.shift();
} else {
// use the raw event for any other fields
val = ev.event;
}
while (parts.length > 0) {
var thispart = parts.shift();
if (!val[thispart]) { return null; }
@@ -215,7 +230,7 @@ function PushProcessor(client) {
var matchingRuleForEventWithRulesets = function(ev, rulesets) {
if (!rulesets || !rulesets.device) { return null; }
if (ev.user_id == client.credentials.userId) { return null; }
if (ev.getSender() == client.credentials.userId) { return null; }
var allDevNames = Object.keys(rulesets.device);
for (var i = 0; i < allDevNames.length; ++i) {
@@ -228,25 +243,11 @@ function PushProcessor(client) {
return matchingRuleFromKindSet(ev, rulesets.global);
};
var actionListToActionsObject = function(actionlist) {
var actionobj = { 'notify': false, 'tweaks': {} };
for (var i = 0; i < actionlist.length; ++i) {
var action = actionlist[i];
if (action === 'notify') {
actionobj.notify = true;
} else if (typeof action === 'object') {
if (action.value === undefined) { action.value = true; }
actionobj.tweaks[action.set_tweak] = action.value;
}
}
return actionobj;
};
var pushActionsForEventAndRulesets = function(ev, rulesets) {
var rule = matchingRuleForEventWithRulesets(ev, rulesets);
if (!rule) { return {}; }
var actionObj = actionListToActionsObject(rule.actions);
var actionObj = PushProcessor.actionListToActionsObject(rule.actions);
// Some actions are implicit in some situations: we add those here
if (actionObj.tweaks.highlight === undefined) {
@@ -258,11 +259,40 @@ function PushProcessor(client) {
return actionObj;
};
/**
* Get the user's push actions for the given event
*
* @param {module:models/event.MatrixEvent} ev
*
* @return {PushAction}
*/
this.actionsForEvent = function(ev) {
return pushActionsForEventAndRulesets(ev, client.pushRules);
};
}
/**
* Convert a list of actions into a object with the actions as keys and their values
* eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ]
* becomes { notify: true, tweaks: { sound: 'default' } }
* @param {array} actionlist The actions list
*
* @return {object} A object with key 'notify' (true or false) and an object of actions
*/
PushProcessor.actionListToActionsObject = function(actionlist) {
var actionobj = { 'notify': false, 'tweaks': {} };
for (var i = 0; i < actionlist.length; ++i) {
var action = actionlist[i];
if (action === 'notify') {
actionobj.notify = true;
} else if (typeof action === 'object') {
if (action.value === undefined) { action.value = true; }
actionobj.tweaks[action.set_tweak] = action.value;
}
}
return actionobj;
};
/**
* @typedef {Object} PushAction
* @type {Object}
@@ -276,3 +306,4 @@ function PushProcessor(client) {
/** The PushProcessor class. */
module.exports = PushProcessor;
+203
View File
@@ -0,0 +1,203 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* A re-implementation of the javascript callback functions (setTimeout,
* clearTimeout; setInterval and clearInterval are not yet implemented) which
* try to improve handling of large clock jumps (as seen when
* suspending/resuming the system).
*
* In particular, if a timeout would have fired while the system was suspended,
* it will instead fire as soon as possible after resume.
*/
"use strict";
// we schedule a callback at least this often, to check if we've missed out on
// some wall-clock time due to being suspended.
var TIMER_CHECK_PERIOD_MS = 1000;
// counter, for making up ids to return from setTimeout
var _count = 0;
// the key for our callback with the real global.setTimeout
var _realCallbackKey;
// a sorted list of the callbacks to be run.
// each is an object with keys [runAt, func, params, key].
var _callbackList = [];
// var debuglog = console.log.bind(console);
var debuglog = function() {};
/**
* Replace the function used by this module to get the current time.
*
* Intended for use by the unit tests.
*
* @param {function} f function which should return a millisecond counter
*
* @internal
*/
module.exports.setNow = function(f) {
_now = f || Date.now;
};
var _now = Date.now;
/**
* reimplementation of window.setTimeout, which will call the callback if
* the wallclock time goes past the deadline.
*
* @param {function} func callback to be called after a delay
* @param {Number} delayMs number of milliseconds to delay by
*
* @return {Number} an identifier for this callback, which may be passed into
* clearTimeout later.
*/
module.exports.setTimeout = function(func, delayMs) {
delayMs = delayMs || 0;
if (delayMs < 0) {
delayMs = 0;
}
var params = Array.prototype.slice.call(arguments, 2);
var runAt = _now() + delayMs;
var key = _count++;
debuglog("setTimeout: scheduling cb", key, "at", runAt,
"(delay", delayMs, ")");
var data = {
runAt: runAt,
func: func,
params: params,
key: key,
};
// figure out where it goes in the list
var idx = binarySearch(
_callbackList, function(el) {
return el.runAt - runAt;
}
);
_callbackList.splice(idx, 0, data);
_scheduleRealCallback();
return key;
};
/**
* reimplementation of window.clearTimeout, which mirrors setTimeout
*
* @param {Number} key result from an earlier setTimeout call
*/
module.exports.clearTimeout = function(key) {
if (_callbackList.length === 0) {
return;
}
// remove the element from the list
var i;
for (i = 0; i < _callbackList.length; i++) {
var cb = _callbackList[i];
if (cb.key == key) {
_callbackList.splice(i, 1);
break;
}
}
// iff it was the first one in the list, reschedule our callback.
if (i === 0) {
_scheduleRealCallback();
}
};
// use the real global.setTimeout to schedule a callback to _runCallbacks.
function _scheduleRealCallback() {
if (_realCallbackKey) {
global.clearTimeout(_realCallbackKey);
}
var first = _callbackList[0];
if (!first) {
debuglog("_scheduleRealCallback: no more callbacks, not rescheduling");
return;
}
var now = _now();
var delayMs = Math.min(first.runAt - now, TIMER_CHECK_PERIOD_MS);
debuglog("_scheduleRealCallback: now:", now, "delay:", delayMs);
_realCallbackKey = global.setTimeout(_runCallbacks, delayMs);
}
function _runCallbacks() {
var cb;
var now = _now();
debuglog("_runCallbacks: now:", now);
// get the list of things to call
var callbacksToRun = [];
while (true) {
var first = _callbackList[0];
if (!first || first.runAt > now) {
break;
}
cb = _callbackList.shift();
debuglog("_runCallbacks: popping", cb.key);
callbacksToRun.push(cb);
}
// reschedule the real callback before running our functions, to
// keep the codepaths the same whether or not our functions
// register their own setTimeouts.
_scheduleRealCallback();
for (var i = 0; i < callbacksToRun.length; i++) {
cb = callbacksToRun[i];
try {
cb.func.apply(null, cb.params);
} catch (e) {
console.error("Uncaught exception in callback function",
e.stack || e);
}
}
}
/* search in a sorted array.
*
* returns the index of the last element for which func returns
* greater than zero, or array.length if no such element exists.
*/
function binarySearch(array, func) {
// min is inclusive, max exclusive.
var min = 0,
max = array.length;
while (min < max) {
var mid = (min + max) >> 1;
var res = func(array[mid]);
if (res > 0) {
// the element at 'mid' is too big; set it as the new max.
max = mid;
} else {
// the element at 'mid' is too small. 'min' is inclusive, so +1.
min = mid + 1;
}
}
// presumably, min==max now.
return min;
}
+31 -1
View File
@@ -43,6 +43,9 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
// filterId: Filter
// }
};
this.accountData = {
// type : content
};
this.localStorage = opts.localStorage;
};
@@ -98,6 +101,11 @@ module.exports.MatrixInMemoryStore.prototype = {
var user = this.users[member.userId] || new User(member.userId);
if (member.name) {
user.setDisplayName(member.name);
if (member.events.member) {
user.setRawDisplayName(
member.events.member.getDirectionalContent().displayname
);
}
}
if (member.events.member && member.events.member.getContent().avatar_url) {
user.setAvatarUrl(member.events.member.getContent().avatar_url);
@@ -244,7 +252,29 @@ module.exports.MatrixInMemoryStore.prototype = {
this.localStorage.setItem("mxjssdk_memory_filter_" + filterName, filterId);
}
catch (e) {}
}
},
/**
* Store user-scoped account data events.
* N.B. that account data only allows a single event per type, so multiple
* events with the same type will replace each other.
* @param {Array<MatrixEvent>} events The events to store.
*/
storeAccountDataEvents: function(events) {
var self = this;
events.forEach(function(event) {
self.accountData[event.getType()] = event;
});
},
/**
* Get account data event by event type
* @param {string} eventType The event type being queried
* @return {?MatrixEvent} the user account_data event of given type, if any
*/
getAccountData: function(eventType) {
return this.accountData[eventType];
},
// TODO
//setMaxHistoryPerRoom: function(maxHistory) {},
+31
View File
@@ -62,6 +62,22 @@ WebStorageSessionStore.prototype = {
return this.store.getItem(KEY_END_TO_END_ACCOUNT);
},
/**
* Store a flag indicating that we have announced the new device.
*/
setDeviceAnnounced: function() {
this.store.setItem(KEY_END_TO_END_ANNOUNCED, "true");
},
/**
* Check if the "device announced" flag is set
*
* @return {boolean} true if the "device announced" flag has been set.
*/
getDeviceAnnounced: function() {
return this.store.getItem(KEY_END_TO_END_ANNOUNCED) == "true";
},
/**
* Stores the known devices for a user.
* @param {string} userId The user's ID.
@@ -104,6 +120,16 @@ WebStorageSessionStore.prototype = {
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
},
getEndToEndInboundGroupSession: function(senderKey, sessionId) {
var key = keyEndToEndInboundGroupSession(senderKey, sessionId);
return this.store.getItem(key);
},
storeEndToEndInboundGroupSession: function(senderKey, sessionId, pickledSession) {
var key = keyEndToEndInboundGroupSession(senderKey, sessionId);
return this.store.setItem(key, pickledSession);
},
/**
* Store the end-to-end state for a room.
* @param {string} roomId The room's ID.
@@ -124,6 +150,7 @@ WebStorageSessionStore.prototype = {
};
var KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
var KEY_END_TO_END_ANNOUNCED = E2E_PREFIX + "announced";
function keyEndToEndDevicesForUser(userId) {
return E2E_PREFIX + "devices/" + userId;
@@ -133,6 +160,10 @@ function keyEndToEndSessions(deviceKey) {
return E2E_PREFIX + "sessions/" + deviceKey;
}
function keyEndToEndInboundGroupSession(senderKey, sessionId) {
return E2E_PREFIX + "inboundgroupsessions/" + senderKey + "/" + sessionId;
}
function keyEndToEndRoom(roomId) {
return E2E_PREFIX + "rooms/" + roomId;
}
+17 -1
View File
@@ -162,7 +162,23 @@ StubStore.prototype = {
*/
setFilterIdByName: function(filterName, filterId) {
}
},
/**
* Store user-scoped account data events
* @param {Array<MatrixEvent>} events The events to store.
*/
storeAccountDataEvents: function(events) {
},
/**
* Get account data event by event type
* @param {string} eventType The event type being queried
*/
getAccountData: function(eventType) {
},
// TODO
//setMaxHistoryPerRoom: function(maxHistory) {},
+2 -2
View File
@@ -373,7 +373,7 @@ WebStorageStore.prototype.scrollback = function(room, limit) {
);
room.addEventsToTimeline(utils.map(scrollback, function(e) {
return new MatrixEvent(e);
}), true);
}), true, room.getLiveTimeline());
this._tokens[room.storageToken] = {
earliestIndex: earliestIndex
@@ -594,7 +594,7 @@ function loadRoom(store, roomId, numEvents, tokenArray) {
index--;
}
// add events backwards to diverge old state correctly.
room.addEventsToTimeline(recentEvents.reverse(), true);
room.addEventsToTimeline(recentEvents.reverse(), true, room.getLiveTimeline());
room.oldState.paginationToken = currentStateMap.pagination_token;
// set the token data to let us know which index this room instance is at
// for scrollback.
+146 -86
View File
@@ -72,6 +72,12 @@ function SyncApi(client, opts) {
this._running = false;
this._keepAliveTimer = null;
this._connectionReturnedDefer = null;
this._notifEvents = []; // accumulator of sync events in the current sync response
if (client.getNotifTimelineSet()) {
reEmit(client, client.getNotifTimelineSet(),
["Room.timeline", "Room.timelineReset"]);
}
}
/**
@@ -88,6 +94,7 @@ SyncApi.prototype.createRoom = function(roomId) {
"Room.receipt", "Room.tags",
"Room.timelineReset",
"Room.localEchoUpdated",
"Room.accountData",
]);
this._registerStateListeners(room);
return room;
@@ -147,7 +154,7 @@ SyncApi.prototype.syncLeftRooms = function() {
timeout: 0 // don't want to block since this is a single isolated req
};
return this._getOrCreateFilter(
return client.getOrCreateFilter(
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter
).then(function(filterId) {
qps.filter = filterId;
@@ -262,7 +269,8 @@ SyncApi.prototype.peek = function(roomId) {
// will overwrite the pagination token, so make sure it overwrites
// it with the right thing.
peekRoom.addEventsToTimeline(messages.reverse(), true,
undefined, response.messages.start);
peekRoom.getLiveTimeline(),
response.messages.start);
client.store.storeRoom(peekRoom);
client.emit("Room", peekRoom);
@@ -328,7 +336,7 @@ SyncApi.prototype._peekPoll = function(roomId, token) {
return e.room_id === roomId;
}).map(self.client.getEventMapper());
var room = self.client.getRoom(roomId);
room.addEvents(events);
room.addLiveEvents(events);
self._peekPoll(roomId, res.end);
}, function(err) {
console.error("[%s] Peek poll failed: %s", roomId, err);
@@ -351,7 +359,9 @@ SyncApi.prototype.getSyncState = function() {
* Main entry point
*/
SyncApi.prototype.sync = function() {
debuglog("SyncApi.sync");
debuglog("SyncApi.sync: starting with sync token " +
this.client.store.getSyncToken());
var client = this.client;
var self = this;
@@ -385,9 +395,15 @@ SyncApi.prototype.sync = function() {
var filter = new Filter(client.credentials.userId);
filter.setTimelineLimit(self.opts.initialSyncLimit);
self._getOrCreateFilter(
client.getOrCreateFilter(
getFilterName(client.credentials.userId), filter
).done(function(filterId) {
// reset the notifications timeline to prepare it to paginate from
// the current point in time.
// The right solution would be to tie /sync pagination tokens into
// /notifications API somehow.
client.resetNotifTimelineSet();
self._sync({ filterId: filterId });
}, function(err) {
self._startKeepAlives().done(function() {
@@ -417,6 +433,10 @@ SyncApi.prototype.stop = function() {
}
this._running = false;
if (this._currentSyncRequest) { this._currentSyncRequest.abort(); }
if (this._keepAliveTimer) {
clearTimeout(this._keepAliveTimer);
this._keepAliveTimer = null;
}
};
/**
@@ -447,6 +467,7 @@ SyncApi.prototype._sync = function(syncOptions) {
self._connectionReturnedDefer = null;
}
this._updateSyncState("STOPPED");
return;
}
var filterId = syncOptions.filterId;
@@ -459,9 +480,17 @@ SyncApi.prototype._sync = function(syncOptions) {
var qps = {
filter: filterId,
timeout: this.opts.pollTimeout,
since: syncToken || undefined // do not send 'null'
};
if (syncToken) {
qps.since = syncToken;
} else {
// use a cachebuster for initialsyncs, to make sure that
// we don't get a stale sync
// (https://github.com/vector-im/vector-web/issues/1354)
qps._cacheBuster = Date.now();
}
if (self._syncConnectionLost) {
// we think the connection is dead. If it comes back up, we won't know
// about it till /sync returns. If the timeout= is high, this could
@@ -479,36 +508,6 @@ SyncApi.prototype._sync = function(syncOptions) {
this._currentSyncRequest.done(function(data) {
self._syncConnectionLost = false;
// data looks like:
// {
// next_batch: $token,
// presence: { events: [] },
// rooms: {
// invite: {
// $roomid: {
// invite_state: { events: [] }
// }
// },
// join: {
// $roomid: {
// state: { events: [] },
// timeline: { events: [], prev_batch: $token, limited: true },
// ephemeral: { events: [] },
// account_data: { events: [] },
// unread_notifications: {
// highlight_count: 0,
// notification_count: 0,
// }
// }
// },
// leave: {
// $roomid: {
// state: { events: [] },
// timeline: { events: [], prev_batch: $token }
// }
// }
// }
// }
// set the sync token NOW *before* processing the events. We do this so
// if something barfs on an event we can skip it rather than constantly
@@ -568,6 +567,39 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
var client = this.client;
var self = this;
// data looks like:
// {
// next_batch: $token,
// presence: { events: [] },
// account_data: { events: [] },
// to_device: { events: [] },
// rooms: {
// invite: {
// $roomid: {
// invite_state: { events: [] }
// }
// },
// join: {
// $roomid: {
// state: { events: [] },
// timeline: { events: [], prev_batch: $token, limited: true },
// ephemeral: { events: [] },
// account_data: { events: [] },
// unread_notifications: {
// highlight_count: 0,
// notification_count: 0,
// }
// }
// },
// leave: {
// $roomid: {
// state: { events: [] },
// timeline: { events: [], prev_batch: $token }
// }
// }
// },
// }
// TODO-arch:
// - Each event we pass through needs to be emitted via 'event', can we
// do this in one place?
@@ -590,7 +622,44 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
});
}
// the returned json structure is abit crap, so make it into a
// handle non-room account_data
if (data.account_data && utils.isArray(data.account_data.events)) {
var events = data.account_data.events.map(client.getEventMapper());
client.store.storeAccountDataEvents(events);
events.forEach(
function(accountDataEvent) {
if (accountDataEvent.getType() == 'm.push_rules') {
client.pushRules = accountDataEvent.getContent();
}
client.emit("accountData", accountDataEvent);
return accountDataEvent;
}
);
}
// handle to-device events
if (data.to_device && utils.isArray(data.to_device.events)) {
data.to_device.events
.map(client.getEventMapper())
.forEach(
function(toDeviceEvent) {
var content = toDeviceEvent.getContent();
if (
toDeviceEvent.getType() == "m.room.message" &&
content.msgtype == "m.bad.encrypted"
) {
console.warn(
"Unable to decrypt to-device event: " + content.body
);
return;
}
client.emit("toDeviceEvent", toDeviceEvent);
}
);
}
// the returned json structure is a bit crap, so make it into a
// nicer form (array) after applying sanity to make sure we don't fail
// on missing keys (on the off chance)
var inviteRooms = [];
@@ -609,6 +678,8 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
}
}
this._notifEvents = [];
// Handle invites
inviteRooms.forEach(function(inviteObj) {
var room = inviteObj.room;
@@ -694,6 +765,12 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
room.currentState.paginationToken = syncToken;
self._deregisterStateListeners(room);
room.resetLiveTimeline(joinObj.timeline.prev_batch);
// We have to assume any gap in any timeline is
// reason to stop incrementally tracking notifications and
// reset the timeline.
client.resetNotifTimelineSet();
self._registerStateListeners(room);
}
}
@@ -703,7 +780,7 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
// XXX: should we be adding ephemeralEvents to the timeline?
// It feels like that for symmetry with room.addAccountData()
// there should be a room.addEphemeralEvents() or similar.
room.addEvents(ephemeralEvents);
room.addLiveEvents(ephemeralEvents);
// we deliberately don't add accountData to the timeline
room.addAccountData(accountDataEvents);
@@ -742,6 +819,20 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
timelineEvents.forEach(function(e) { client.emit("event", e); });
accountDataEvents.forEach(function(e) { client.emit("event", e); });
});
// update the notification timeline, if appropriate.
// we only do this for live events, as otherwise we can't order them sanely
// in the timeline relative to ones paginated in by /notifications.
// XXX: we could fix this by making EventTimeline support chronological
// ordering... but it doesn't, right now.
if (syncToken && this._notifEvents.length) {
this._notifEvents.sort(function(a, b) {
return a.getTs() - b.getTs();
});
this._notifEvents.forEach(function(event) {
client.getNotifTimelineSet().addLiveEvent(event);
});
}
};
/**
@@ -805,52 +896,6 @@ SyncApi.prototype._pokeKeepAlive = function() {
});
};
/**
* @param {string} filterName
* @param {Filter} filter
* @return {Promise<String>} Filter ID
*/
SyncApi.prototype._getOrCreateFilter = function(filterName, filter) {
var client = this.client;
var filterId = client.store.getFilterIdByName(filterName);
var promise = q();
if (filterId) {
// check that the existing filter matches our expectations
promise = client.getFilter(client.credentials.userId,
filterId, true
).then(function(existingFilter) {
var oldStr = JSON.stringify(existingFilter.getDefinition());
var newStr = JSON.stringify(filter.getDefinition());
if (oldStr == newStr) {
// super, just use that.
debuglog("Using existing filter ID %s: %s", filterId, oldStr);
return q(filterId);
}
debuglog("Existing filter ID %s: %s; new filter: %s",
filterId, oldStr, newStr);
return;
});
}
return promise.then(function(existingId) {
if (existingId) {
return existingId;
}
// create a new filter
return client.createFilter(filter.getDefinition()
).then(function(createdFilter) {
debuglog("Created new filter ID %s: %s", createdFilter.filterId,
JSON.stringify(createdFilter.getDefinition()));
client.store.setFilterIdByName(filterName, createdFilter.filterId);
return createdFilter.filterId;
});
});
};
/**
* @param {Object} obj
* @return {Object[]}
@@ -976,9 +1021,21 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
// may make notifications appear which should have the right name.
room.recalculate(this.client.credentials.userId);
// gather our notifications into this._notifEvents
if (client.getNotifTimelineSet()) {
for (var i = 0; i < timelineEventList.length; i++) {
var pushActions = client.getPushActionsForEvent(timelineEventList[i]);
if (pushActions && pushActions.notify &&
pushActions.tweaks && pushActions.tweaks.highlight)
{
this._notifEvents.push(timelineEventList[i]);
}
}
}
// execute the timeline events, this will begin to diverge the current state
// if the timeline has any state events in it.
room.addEventsToTimeline(timelineEventList);
room.addLiveEvents(timelineEventList);
};
/**
@@ -1024,7 +1081,10 @@ SyncApi.prototype._onOnline = function() {
function createNewUser(client, userId) {
var user = new User(userId);
reEmit(client, user, ["User.avatarUrl", "User.displayName", "User.presence"]);
reEmit(client, user, [
"User.avatarUrl", "User.displayName", "User.presence",
"User.currentlyActive", "User.lastPresenceTs"
]);
return user;
}
+32 -9
View File
@@ -30,11 +30,18 @@ var DEBUG = false;
*/
var debuglog = DEBUG ? console.log.bind(console) : function() {};
/**
* the number of times we ask the server for more events before giving up
*
* @private
*/
var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
/**
* Construct a TimelineWindow.
*
* <p>This abstracts the separate timelines in a Matrix {@link
* module:models/room~Room|Room} into a single iterable thing. It keeps track of
* module:models/room|Room} into a single iterable thing. It keeps track of
* the start and endpoints of the window, which can be advanced with the help
* of pagination requests.
*
@@ -49,7 +56,7 @@ var debuglog = DEBUG ? console.log.bind(console) : function() {};
* @param {MatrixClient} client MatrixClient to be used for context/pagination
* requests.
*
* @param {Room} room The room to track
* @param {EventTimelineSet} timelineSet The timelineSet to track
*
* @param {Object} [opts] Configuration options for this window
*
@@ -59,10 +66,10 @@ var debuglog = DEBUG ? console.log.bind(console) : function() {};
*
* @constructor
*/
function TimelineWindow(client, room, opts) {
function TimelineWindow(client, timelineSet, opts) {
opts = opts || {};
this._client = client;
this._room = room;
this._timelineSet = timelineSet;
// these will be TimelineIndex objects; they delineate the 'start' and
// 'end' of the window.
@@ -106,7 +113,7 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
// TODO: ideally we'd spot getEventTimeline returning a resolved promise and
// skip straight to the find-event loop.
if (initialEventId) {
return this._client.getEventTimeline(this._room, initialEventId)
return this._client.getEventTimeline(this._timelineSet, initialEventId)
.then(function(tl) {
// make sure that our window includes the event
for (var i = 0; i < tl.getEvents().length; i++) {
@@ -119,7 +126,7 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
});
} else {
// start with the most recent events
var tl = this._room.getLiveTimeline();
var tl = this._timelineSet.getLiveTimeline();
initFields(tl, tl.getEvents().length);
return q();
}
@@ -179,10 +186,14 @@ TimelineWindow.prototype.canPaginate = function(direction) {
* even if there are fewer than 'size' of them, as we will just return those
* we already know about.)
*
* @param {number} [requestLimit = 5] limit for the number of API requests we
* should make.
*
* @return {module:client.Promise} Resolves to a boolean which is true if more events
* were successfully retrieved.
*/
TimelineWindow.prototype.paginate = function(direction, size, makeRequest) {
TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
requestLimit) {
// Either wind back the message cap (if there are enough events in the
// timeline to do so), or fire off a pagination request.
@@ -190,6 +201,10 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest) {
makeRequest = true;
}
if (requestLimit === undefined) {
requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT;
}
var tl;
if (direction == EventTimeline.BACKWARDS) {
tl = this._start;
@@ -224,7 +239,9 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest) {
return q(true);
}
if (!makeRequest) {
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);
}
@@ -237,6 +254,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest) {
debuglog("TimelineWindow: starting request");
var self = this;
var prom = this._client.paginateEventTimeline(tl.timeline, {
backwards: direction == EventTimeline.BACKWARDS,
limit: size
@@ -256,7 +274,12 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest) {
// token. In particular, we want to know if we've actually hit the
// start of the timeline, or if we just happened to know about all of
// the events thanks to https://matrix.org/jira/browse/SYN-645.
return self.paginate(direction, size, true);
//
// On the other hand, we necessarily want to wait forever for the
// server to make its mind up about whether there are other events,
// because it gives a bad user experience
// (https://github.com/vector-im/vector-web/issues/1204).
return self.paginate(direction, size, true, requestLimit - 1);
});
tl.pendingPaginate = prom;
return prom;
+110
View File
@@ -246,6 +246,116 @@ module.exports.deepCopy = function(obj) {
return JSON.parse(JSON.stringify(obj));
};
/**
* Compare two objects for equality. The objects MUST NOT have circular references.
*
* @param {Object} x The first object to compare.
* @param {Object} y The second object to compare.
*
* @return {boolean} true if the two objects are equal
*/
var deepCompare = module.exports.deepCompare = function(x, y) {
// Inspired by
// http://stackoverflow.com/questions/1068834/object-comparison-in-javascript#1144249
// Compare primitives and functions.
// Also check if both arguments link to the same object.
if (x === y) {
return true;
}
if (typeof x !== typeof y) {
return false;
}
// special-case NaN (since NaN !== NaN)
if (typeof x === 'number' && isNaN(x) && isNaN(y)) {
return true;
}
// special-case null (since typeof null == 'object', but null.constructor
// throws)
if (x === null || y === null) {
return x === y;
}
// everything else is either an unequal primitive, or an object
if (!(x instanceof Object)) {
return false;
}
// check they are the same type of object
if (x.constructor !== y.constructor || x.prototype !== y.prototype) {
return false;
}
// special-casing for some special types of object
if (x instanceof RegExp || x instanceof Date) {
return x.toString() === y.toString();
}
// the object algorithm works for Array, but it's sub-optimal.
if (x instanceof Array) {
if (x.length !== y.length) {
return false;
}
for (var i = 0; i < x.length; i++) {
if (!deepCompare(x[i], y[i])) {
return false;
}
}
} else {
// disable jshint "The body of a for in should be wrapped in an if
// statement"
/* jshint -W089 */
// check that all of y's direct keys are in x
var p;
for (p in y) {
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
}
// finally, compare each of x's keys with y
for (p in y) {
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
if (!deepCompare(x[p], y[p])) {
return false;
}
}
}
/* jshint +W089 */
return true;
};
/**
* Copy properties from one object to another.
*
* All enumerable properties, included inherited ones, are copied.
*
* @param {Object} target The object that will receive new properties
* @param {...Object} source Objects from which to copy properties
*
* @return {Object} target
*/
module.exports.extend = function() {
var target = arguments[0] || {};
// disable jshint "The body of a for in should be wrapped in an if
// statement"
/* jshint -W089 */
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var propName in source) {
target[propName] = source[propName];
}
}
/* jshint +W089 */
return target;
};
/**
* Run polyfills to add Array.map and Array.filter if they are missing.
+128 -18
View File
@@ -60,6 +60,12 @@ function MatrixCall(opts) {
this.candidateSendQueue = [];
this.candidateSendTries = 0;
// Lookup from opaque queue ID to a promise for media element operations that
// need to be serialised into a given queue. Store this per-MatrixCall on the
// assumption that multiple matrix calls will never compete for control of the
// same DOM elements.
this.mediaPromises = Object.create(null);
this.screenSharingStream = null;
}
/** The length of time a call can be ringing for. */
@@ -144,6 +150,86 @@ MatrixCall.prototype.placeScreenSharingCall =
_tryPlayRemoteStream(this);
};
/**
* Play the given HTMLMediaElement, serialising the operation into a chain
* of promises to avoid racing access to the element
* @param {Element} HTMLMediaElement element to play
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
*/
MatrixCall.prototype.playElement = function(element, queueId) {
console.log("queuing play on " + queueId + " and element " + element);
// XXX: FIXME: Does this leak elements, given the old promises
// may hang around and retain a reference to them?
if (this.mediaPromises[queueId]) {
// XXX: these promises can fail (e.g. by <video/> being unmounted whilst
// pending receiving media to play - e.g. whilst switching between
// rooms before answering an inbound call), and throw unhandled exceptions.
// However, we should soldier on as best we can even if they fail, given
// these failures may be non-fatal (as in the case of unmounts)
this.mediaPromises[queueId] =
this.mediaPromises[queueId].then(function() {
console.log("previous promise completed for " + queueId);
return element.play();
}, function() {
console.log("previous promise failed for " + queueId);
return element.play();
});
}
else {
this.mediaPromises[queueId] = element.play();
}
};
/**
* Pause the given HTMLMediaElement, serialising the operation into a chain
* of promises to avoid racing access to the element
* @param {Element} HTMLMediaElement element to pause
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
*/
MatrixCall.prototype.pauseElement = function(element, queueId) {
console.log("queuing pause on " + queueId + " and element " + element);
if (this.mediaPromises[queueId]) {
this.mediaPromises[queueId] =
this.mediaPromises[queueId].then(function() {
console.log("previous promise completed for " + queueId);
return element.pause();
}, function() {
console.log("previous promise failed for " + queueId);
return element.pause();
});
}
else {
// pause doesn't actually return a promise, but do this for symmetry
// and just in case it does in future.
this.mediaPromises[queueId] = element.pause();
}
};
/**
* Assign the given HTMLMediaElement by setting the .src attribute on it,
* serialising the operation into a chain of promises to avoid racing access
* to the element
* @param {Element} HTMLMediaElement element to pause
* @param {string} src the src attribute value to assign to the element
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
*/
MatrixCall.prototype.assignElement = function(element, src, queueId) {
console.log("queuing assign on " + queueId + " element " + element + " for " + src);
if (this.mediaPromises[queueId]) {
this.mediaPromises[queueId] =
this.mediaPromises[queueId].then(function() {
console.log("previous promise completed for " + queueId);
element.src = src;
}, function() {
console.log("previous promise failed for " + queueId);
element.src = src;
});
}
else {
element.src = src;
}
};
/**
* Retrieve the local <code>&lt;video&gt;</code> DOM element.
* @return {Element} The dom element
@@ -180,13 +266,15 @@ MatrixCall.prototype.setLocalVideoElement = function(element) {
if (element && this.localAVStream && this.type === 'video') {
element.autoplay = true;
element.src = this.URL.createObjectURL(this.localAVStream);
this.assignElement(element,
this.URL.createObjectURL(this.localAVStream),
"localVideo");
element.muted = true;
var self = this;
setTimeout(function() {
var vel = self.getLocalVideoElement();
if (vel.play) {
vel.play();
self.playElement(vel, "localVideo");
}
}, 0);
}
@@ -205,9 +293,11 @@ MatrixCall.prototype.setRemoteVideoElement = function(element) {
/**
* Set the remote <code>&lt;audio&gt;</code> DOM element. If this call is active,
* the first received audio-only stream will be rendered to it immediately.
* The audio will *not* be rendered from the remoteVideoElement.
* @param {Element} element The <code>&lt;video&gt;</code> DOM element.
*/
MatrixCall.prototype.setRemoteAudioElement = function(element) {
this.remoteVideoElement.muted = true;
this.remoteAudioElement = element;
_tryPlayRemoteAudioStream(this);
};
@@ -414,16 +504,20 @@ MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
videoEl.autoplay = true;
if (this.screenSharingStream) {
debuglog("Setting screen sharing stream to the local video element");
videoEl.src = this.URL.createObjectURL(this.screenSharingStream);
this.assignElement(videoEl,
this.URL.createObjectURL(this.screenSharingStream),
"localVideo");
}
else {
videoEl.src = this.URL.createObjectURL(stream);
this.assignElement(videoEl,
this.URL.createObjectURL(stream),
"localVideo");
}
videoEl.muted = true;
setTimeout(function() {
var vel = self.getLocalVideoElement();
if (vel.play) {
vel.play();
self.playElement(vel, "localVideo");
}
}, 0);
}
@@ -460,12 +554,14 @@ MatrixCall.prototype._gotUserMediaForAnswer = function(stream) {
if (localVidEl && self.type == 'video') {
localVidEl.autoplay = true;
localVidEl.src = self.URL.createObjectURL(stream);
this.assignElement(localVidEl,
this.URL.createObjectURL(stream),
"localVideo");
localVidEl.muted = true;
setTimeout(function() {
var vel = self.getLocalVideoElement();
if (vel.play) {
vel.play();
self.playElement(vel, "localVideo");
}
}, 0);
}
@@ -700,6 +796,7 @@ MatrixCall.prototype._onAddStream = function(event) {
if (s.getVideoTracks().length > 0) {
this.type = 'video';
this.remoteAVStream = s;
this.remoteAStream = s;
} else {
this.type = 'voice';
this.remoteAStream = s;
@@ -712,12 +809,20 @@ MatrixCall.prototype._onAddStream = function(event) {
t.onstarted = hookCallback(self, self._onRemoteStreamTrackStarted);
});
event.stream.onended = hookCallback(self, self._onRemoteStreamEnded);
if (event.stream.oninactive !== undefined) {
event.stream.oninactive = hookCallback(self, self._onRemoteStreamEnded);
}
else {
// onended is deprecated from Chrome 54
event.stream.onended = hookCallback(self, self._onRemoteStreamEnded);
}
// not currently implemented in chrome
event.stream.onstarted = hookCallback(self, self._onRemoteStreamStarted);
if (this.type === 'video') {
_tryPlayRemoteStream(this);
_tryPlayRemoteAudioStream(this);
}
else {
_tryPlayRemoteAudioStream(this);
@@ -824,21 +929,21 @@ var sendCandidate = function(self, content) {
var terminate = function(self, hangupParty, hangupReason, shouldEmit) {
if (self.getRemoteVideoElement()) {
if (self.getRemoteVideoElement().pause) {
self.getRemoteVideoElement().pause();
self.pauseElement(self.getRemoteVideoElement(), "remoteVideo");
}
self.getRemoteVideoElement().src = "";
self.assignElement(self.getRemoteVideoElement(), "", "remoteVideo");
}
if (self.getRemoteAudioElement()) {
if (self.getRemoteAudioElement().pause) {
self.getRemoteAudioElement().pause();
self.pauseElement(self.getRemoteAudioElement(), "remoteAudio");
}
self.getRemoteAudioElement().src = "";
self.assignElement(self.getRemoteAudioElement(), "", "remoteAudio");
}
if (self.getLocalVideoElement()) {
if (self.getLocalVideoElement().pause) {
self.getLocalVideoElement().pause();
self.pauseElement(self.getLocalVideoElement(), "localVideo");
}
self.getLocalVideoElement().src = "";
self.assignElement(self.getLocalVideoElement(), "", "localVideo");
}
self.hangupParty = hangupParty;
self.hangupReason = hangupReason;
@@ -896,11 +1001,13 @@ var _tryPlayRemoteStream = function(self) {
if (self.getRemoteVideoElement() && self.remoteAVStream) {
var player = self.getRemoteVideoElement();
player.autoplay = true;
player.src = self.URL.createObjectURL(self.remoteAVStream);
self.assignElement(player,
self.URL.createObjectURL(self.remoteAVStream),
"remoteVideo");
setTimeout(function() {
var vel = self.getRemoteVideoElement();
if (vel.play) {
vel.play();
self.playElement(vel, "remoteVideo");
}
// OpenWebRTC does not support oniceconnectionstatechange yet
if (self.webRtc.isOpenWebRTC()) {
@@ -914,11 +1021,13 @@ var _tryPlayRemoteAudioStream = function(self) {
if (self.getRemoteAudioElement() && self.remoteAStream) {
var player = self.getRemoteAudioElement();
player.autoplay = true;
player.src = self.URL.createObjectURL(self.remoteAStream);
self.assignElement(player,
self.URL.createObjectURL(self.remoteAStream),
"remoteAudio");
setTimeout(function() {
var ael = self.getRemoteAudioElement();
if (ael.play) {
ael.play();
self.playElement(ael, "remoteAudio");
}
// OpenWebRTC does not support oniceconnectionstatechange yet
if (self.webRtc.isOpenWebRTC()) {
@@ -1102,6 +1211,7 @@ var forAllTracksOnStream = function(s, f) {
/** The MatrixCall class. */
module.exports.MatrixCall = MatrixCall;
/**
* Create a new Matrix call for the browser.
* @param {MatrixClient} client The client instance to use.
+8 -4
View File
@@ -1,11 +1,11 @@
{
"name": "matrix-js-sdk",
"version": "0.5.1",
"version": "0.5.6",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"test": "istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --forceexit --captureExceptions",
"check": "jasmine-node spec --verbose --junitreport --forceexit --captureExceptions",
"test": "istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --captureExceptions",
"check": "jasmine-node spec --verbose --junitreport --captureExceptions",
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
"build": "jshint -c .jshint lib/ && browserify browser-index.js -o dist/browser-matrix-dev.js --ignore-missing",
"watch": "watchify browser-index.js -o dist/browser-matrix-dev.js -v",
@@ -22,8 +22,9 @@
],
"browser": "browser-index.js",
"author": "matrix.org",
"license": "Apache 2.0",
"license": "Apache-2.0",
"dependencies": {
"another-json": "^0.2.0",
"browser-request": "^0.3.3",
"browserify": "^10.2.3",
"q": "^1.4.1",
@@ -36,5 +37,8 @@
"jshint": "^2.8.0",
"jsdoc": "^3.4.0",
"uglifyjs": "^2.4.10"
},
"optionalDependencies": {
"olm": "https://matrix.org/packages/npm/olm/olm-1.3.0.tgz"
}
}
+1 -1
View File
@@ -90,7 +90,7 @@ if [ -z "$skip_jsdoc" ]; then
echo "copying jsdocs to gh-pages branch"
git checkout gh-pages
git pull
cp -ar ".jsdoc/matrix-js-sdk/$release" .
cp -a ".jsdoc/matrix-js-sdk/$release" .
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' \
$release index.html
+54
View File
@@ -0,0 +1,54 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* A mock implementation of the webstorage api
* @constructor
*/
function MockStorageApi() {
this.data = {};
this.keys = [];
this.length = 0;
}
MockStorageApi.prototype = {
setItem: function(k, v) {
this.data[k] = v;
this._recalc();
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
this._recalc();
},
key: function(index) {
return this.keys[index];
},
_recalc: function() {
var keys = [];
for (var k in this.data) {
if (!this.data.hasOwnProperty(k)) { continue; }
keys.push(k);
}
this.keys = keys;
this.length = keys.length;
}
};
/** */
module.exports = MockStorageApi;
+550 -191
View File
@@ -1,7 +1,10 @@
"use strict";
var sdk = require("../..");
var q = require("q");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
var utils = require("../../lib/utils");
var test_utils = require("../test-utils");
function MockStorageApi() {
this.data = {};
}
@@ -17,233 +20,589 @@ MockStorageApi.prototype = {
}
};
var aliHttpBackend;
var bobHttpBackend;
var aliClient;
var roomId = "!room:localhost";
var aliUserId = "@ali:localhost";
var aliDeviceId = "zxcvb";
var aliAccessToken = "aseukfgwef";
var bobClient;
var bobUserId = "@bob:localhost";
var bobDeviceId = "bvcxz";
var bobAccessToken = "fewgfkuesa";
var bobOneTimeKeys;
var aliDeviceKeys;
var bobDeviceKeys;
var bobDeviceCurve25519Key;
var bobDeviceEd25519Key;
var aliLocalStore;
var aliStorage;
var bobStorage;
var aliMessages;
var bobMessages;
/**
* Set an expectation that the client will upload device keys and a number of
* one-time keys; then flush the http requests.
*
* @param {string} deviceId expected device id in upload request
* @param {object} httpBackend
*
* @return {promise} completes once the http requests have completed, returning combined
* {one_time_keys: {}, device_keys: {}}
*/
function expectKeyUpload(deviceId, httpBackend) {
var uploadPath = "/keys/upload/" + deviceId;
var keys = {};
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.one_time_keys).not.toBeDefined();
expect(content.device_keys).toBeDefined();
keys.device_keys = content.device_keys;
return {one_time_key_counts: {curve25519: 0}};
});
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.device_keys).not.toBeDefined();
expect(content.one_time_keys).toBeDefined();
expect(content.one_time_keys).not.toEqual({});
var count = 0;
for (var key in content.one_time_keys) {
if (content.one_time_keys.hasOwnProperty(key)) {
count++;
}
}
expect(count).toEqual(5);
keys.one_time_keys = content.one_time_keys;
return {one_time_key_counts: {curve25519: count}};
});
return httpBackend.flush(uploadPath, 2).then(function() {
return keys;
});
}
/**
* Set an expectation that ali will upload device keys and a number of one-time keys;
* then flush the http requests.
*
* <p>Updates <tt>aliDeviceKeys</tt>
*
* @return {promise} completes once the http requests have completed.
*/
function expectAliKeyUpload() {
return expectKeyUpload(aliDeviceId, aliHttpBackend).then(function(content) {
aliDeviceKeys = content.device_keys;
});
}
/**
* Set an expectation that bob will upload device keys and a number of one-time keys;
* then flush the http requests.
*
* <p>Updates <tt>bobDeviceKeys</tt>, <tt>bobOneTimeKeys</tt>,
* <tt>bobDeviceCurve25519Key</tt>, <tt>bobDeviceEd25519Key</tt>
*
* @return {promise} completes once the http requests have completed.
*/
function expectBobKeyUpload() {
return expectKeyUpload(bobDeviceId, bobHttpBackend).then(function(content) {
bobDeviceKeys = content.device_keys;
bobOneTimeKeys = content.one_time_keys;
expect(bobDeviceKeys).toBeDefined();
expect(bobOneTimeKeys).toBeDefined();
bobDeviceCurve25519Key = bobDeviceKeys.keys["curve25519:bvcxz"];
bobDeviceEd25519Key = bobDeviceKeys.keys["ed25519:bvcxz"];
});
}
function bobUploadsKeys() {
bobClient.uploadKeys(5).catch(test_utils.failTest);
return expectBobKeyUpload();
}
/**
* Set an expectation that ali will query bobs keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
*/
function expectAliQueryKeys() {
// can't query keys before bob has uploaded them
expect(bobDeviceKeys).toBeDefined();
var bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual({});
var result = {};
result[bobUserId] = bobKeys;
return {device_keys: result};
});
return aliHttpBackend.flush("/keys/query", 1);
}
/**
* Set an expectation that bob will query alis keys; then flush the http request.
*
* @return {promise} which resolves once the http request has completed.
*/
function expectBobQueryKeys() {
// can't query keys before ali has uploaded them
expect(aliDeviceKeys).toBeDefined();
var aliKeys = {};
aliKeys[aliDeviceId] = aliDeviceKeys;
bobHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[aliUserId]).toEqual({});
var result = {};
result[aliUserId] = aliKeys;
return {device_keys: result};
});
return bobHttpBackend.flush("/keys/query", 1);
}
/**
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
*/
function expectAliClaimKeys() {
// can't query keys before bob has uploaded them
expect(bobOneTimeKeys).toBeDefined();
aliHttpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
expect(content.one_time_keys[bobUserId][bobDeviceId]).toEqual("curve25519");
for (var keyId in bobOneTimeKeys) {
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
if (keyId.indexOf("curve25519:") === 0) {
break;
}
}
}
var result = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = bobOneTimeKeys[keyId];
return {one_time_keys: result};
});
return aliHttpBackend.flush("/keys/claim", 1);
}
function aliDownloadsKeys() {
// can't query keys before bob has uploaded them
expect(bobDeviceEd25519Key).toBeDefined();
var p1 = aliClient.downloadKeys([bobUserId]).then(function() {
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([{
id: "bvcxz",
key: bobDeviceEd25519Key,
verified: false,
blocked: false,
display_name: null,
}]);
});
var p2 = expectAliQueryKeys();
// check that the localStorage is updated as we expect (not sure this is
// an integration test, but meh)
return q.all([p1, p2]).then(function() {
var devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
expect(devices[bobDeviceId].keys).toEqual(bobDeviceKeys.keys);
expect(devices[bobDeviceId].verified).
toBe(0); // DeviceVerification.UNVERIFIED
});
}
function aliEnablesEncryption() {
return aliClient.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
}).then(function() {
expect(aliClient.isRoomEncrypted(roomId)).toBeTruthy();
});
}
function bobEnablesEncryption() {
return bobClient.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
}).then(function() {
expect(bobClient.isRoomEncrypted(roomId)).toBeTruthy();
});
}
/**
* Ali sends a message, first claiming e2e keys. Set the expectations and
* check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function aliSendsFirstMessage() {
return q.all([
sendMessage(aliClient),
expectAliQueryKeys()
.then(expectAliClaimKeys)
.then(expectAliSendMessageRequest)
]).spread(function(_, ciphertext) {
return ciphertext;
});
}
/**
* Ali sends a message without first claiming e2e keys. Set the expectations
* and check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function aliSendsMessage() {
return q.all([
sendMessage(aliClient),
expectAliSendMessageRequest()
]).spread(function(_, ciphertext) {
return ciphertext;
});
}
/**
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
* expectations and check the results.
*
* @return {promise} which resolves to the ciphertext for Ali's device.
*/
function bobSendsReplyMessage() {
return q.all([
sendMessage(bobClient),
expectBobQueryKeys()
.then(expectBobSendMessageRequest)
]).spread(function(_, ciphertext) {
return ciphertext;
});
}
/**
* Set an expectation that Ali will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function expectAliSendMessageRequest() {
return expectSendMessageRequest(aliHttpBackend).then(function(content) {
aliMessages.push(content);
expect(utils.keys(content.ciphertext)).toEqual([bobDeviceCurve25519Key]);
var ciphertext = content.ciphertext[bobDeviceCurve25519Key];
expect(ciphertext).toBeDefined();
return ciphertext;
});
}
/**
* Set an expectation that Bob will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function expectBobSendMessageRequest() {
return expectSendMessageRequest(bobHttpBackend).then(function(content) {
bobMessages.push(content);
var aliKeyId = "curve25519:" + aliDeviceId;
var aliDeviceCurve25519Key = aliDeviceKeys.keys[aliKeyId];
expect(utils.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
var ciphertext = content.ciphertext[aliDeviceCurve25519Key];
expect(ciphertext).toBeDefined();
return ciphertext;
});
}
function sendMessage(client) {
return client.sendMessage(
roomId, {msgtype: "m.text", body: "Hello, World"}
);
}
function expectSendMessageRequest(httpBackend) {
var path = "/send/m.room.encrypted/";
var sent;
httpBackend.when("PUT", path).respond(200, function(path, content) {
sent = content;
return {
event_id: "asdfgh",
};
});
return httpBackend.flush(path, 1).then(function() {
return sent;
});
}
function aliRecvMessage() {
var message = bobMessages.shift();
return recvMessage(aliHttpBackend, aliClient, message);
}
function bobRecvMessage() {
var message = aliMessages.shift();
return recvMessage(bobHttpBackend, bobClient, message);
}
function recvMessage(httpBackend, client, message) {
var syncData = {
next_batch: "x",
rooms: {
join: {
}
}
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
test_utils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message
})
]
}
};
httpBackend.when("GET", "/sync").respond(200, syncData);
var deferred = q.defer();
var onEvent = function(event) {
console.log(client.credentials.userId + " received event",
event);
// ignore the m.room.member events
if (event.getType() == "m.room.member") {
return;
}
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toEqual({
msgtype: "m.text",
body: "Hello, World"
});
expect(event.isEncrypted()).toBeTruthy();
client.removeListener("event", onEvent);
deferred.resolve();
};
client.on("event", onEvent);
httpBackend.flush();
return deferred.promise;
}
function aliStartClient() {
expectAliKeyUpload().catch(test_utils.failTest);
startClient(aliHttpBackend, aliClient);
return aliHttpBackend.flush().then(function() {
console.log("Ali client started");
});
}
function bobStartClient() {
expectBobKeyUpload().catch(test_utils.failTest);
startClient(bobHttpBackend, bobClient);
return bobHttpBackend.flush().then(function() {
console.log("Bob client started");
});
}
/**
* Set http responses for the requests which are made when a client starts, and
* start the client.
*
* @param {object} httpBackend
* @param {MatrixClient} client
*/
function startClient(httpBackend, client) {
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
// send a sync response including our test room.
var syncData = {
next_batch: "x",
rooms: {
join: { }
}
};
syncData.rooms.join[roomId] = {
state: {
events: [
test_utils.mkMembership({
mship: "join",
user: aliUserId,
}),
test_utils.mkMembership({
mship: "join",
user: bobUserId,
}),
]
},
timeline: {
events: []
}
};
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
}
describe("MatrixClient crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
var baseUrl = "http://localhost.or.something";
var httpBackend;
var aliClient;
var roomId = "!room:localhost";
var aliUserId = "@ali:localhost";
var aliDeviceId = "zxcvb";
var aliAccessToken = "aseukfgwef";
var bobClient;
var bobUserId = "@bob:localhost";
var bobDeviceId = "bvcxz";
var bobAccessToken = "fewgfkuesa";
var bobOneTimeKeys;
var bobDeviceKeys;
var bobDeviceCurve25519Key;
var bobDeviceEd25519Key;
var aliLocalStore;
var aliStorage;
var bobStorage;
var aliMessage;
beforeEach(function() {
aliLocalStore = new MockStorageApi();
aliStorage = new sdk.WebStorageSessionStore(aliLocalStore);
bobStorage = new sdk.WebStorageSessionStore(new MockStorageApi());
utils.beforeEach(this);
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
test_utils.beforeEach(this);
aliHttpBackend = new HttpBackend();
aliClient = sdk.createClient({
baseUrl: baseUrl,
baseUrl: "http://alis.server",
userId: aliUserId,
accessToken: aliAccessToken,
deviceId: aliDeviceId,
sessionStore: aliStorage
sessionStore: aliStorage,
request: aliHttpBackend.requestFn,
});
bobHttpBackend = new HttpBackend();
bobClient = sdk.createClient({
baseUrl: baseUrl,
baseUrl: "http://bobs.server",
userId: bobUserId,
accessToken: bobAccessToken,
deviceId: bobDeviceId,
sessionStore: bobStorage
sessionStore: bobStorage,
request: bobHttpBackend.requestFn,
});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
bobOneTimeKeys = undefined;
aliDeviceKeys = undefined;
bobDeviceKeys = undefined;
bobDeviceCurve25519Key = undefined;
bobDeviceEd25519Key = undefined;
aliMessages = [];
bobMessages = [];
});
describe("Ali account setup", function() {
it("should have device keys", function(done) {
expect(aliClient.deviceKeys).toBeDefined();
expect(aliClient.deviceKeys.user_id).toEqual(aliUserId);
expect(aliClient.deviceKeys.device_id).toEqual(aliDeviceId);
done();
});
it("should have a curve25519 key", function(done) {
expect(aliClient.deviceCurve25519Key).toBeDefined();
done();
});
afterEach(function() {
aliClient.stopClient();
bobClient.stopClient();
});
function bobUploadsKeys(done) {
var uploadPath = "/keys/upload/bvcxz";
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.one_time_keys).toEqual({});
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.one_time_keys).not.toEqual({});
bobDeviceKeys = content.device_keys;
bobOneTimeKeys = content.one_time_keys;
var count = 0;
for (var key in content.one_time_keys) {
if (content.one_time_keys.hasOwnProperty(key)) {
count++;
}
}
expect(count).toEqual(5);
return {one_time_key_counts: {curve25519: count}};
});
return {one_time_key_counts: {}};
});
bobClient.uploadKeys(5);
httpBackend.flush().done(function() {
expect(bobDeviceKeys).toBeDefined();
expect(bobOneTimeKeys).toBeDefined();
bobDeviceCurve25519Key = bobDeviceKeys.keys["curve25519:bvcxz"];
bobDeviceEd25519Key = bobDeviceKeys.keys["ed25519:bvcxz"];
done();
});
}
it("Bob uploads without one-time keys and with one-time keys", bobUploadsKeys);
function aliDownloadsKeys(done) {
var bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
httpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual({});
var result = {};
result[bobUserId] = bobKeys;
return {device_keys: result};
});
aliClient.downloadKeys([bobUserId]).then(function() {
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([{
id: "bvcxz",
key: bobDeviceEd25519Key
}]);
});
httpBackend.flush().done(function() {
var devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
expect(devices).toEqual(bobKeys);
done();
});
}
it("Bob uploads without one-time keys and with one-time keys", function(done) {
q()
.then(bobUploadsKeys)
.catch(test_utils.failTest).done(done);
});
it("Ali downloads Bobs keys", function(done) {
bobUploadsKeys(function() {aliDownloadsKeys(done);});
q()
.then(bobUploadsKeys)
.then(aliDownloadsKeys)
.catch(test_utils.failTest).done(done);
});
function aliEnablesEncryption(done) {
httpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
expect(content.one_time_keys[bobUserId][bobDeviceId]).toEqual("curve25519");
for (var keyId in bobOneTimeKeys) {
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
if (keyId.indexOf("curve25519:") === 0) {
break;
}
}
}
var result = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = bobOneTimeKeys[keyId];
return {one_time_keys: result};
});
aliClient.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
members: [aliUserId, bobUserId]
}).then(function(res) {
expect(res.missingUsers).toEqual([]);
expect(res.missingDevices).toEqual({});
expect(aliClient.isRoomEncrypted(roomId)).toBeTruthy();
done();
});
httpBackend.flush();
}
it("Ali gets keys with an invalid signature", function(done) {
q()
.then(bobUploadsKeys)
.then(function() {
// tamper bob's keys!
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeDefined();
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
return q.all(aliClient.downloadKeys([bobUserId]),
expectAliQueryKeys());
})
.then(function() {
// should get an empty list
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
})
.catch(test_utils.failTest).done(done);
});
it("Ali enables encryption", function(done) {
bobUploadsKeys(function() {
aliDownloadsKeys(function() {
aliEnablesEncryption(done);
});
});
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.catch(test_utils.failTest).done(done);
});
function aliSendsMessage(done) {
var txnId = "a.transaction.id";
var path = "/send/m.room.encrypted/" + txnId;
httpBackend.when("PUT", path).respond(200, function(path, content) {
aliMessage = content;
expect(aliMessage.ciphertext[bobDeviceCurve25519Key]).toBeDefined();
return {};
});
aliClient.sendMessage(
roomId, {msgtype: "m.text", body: "Hello, World"}, txnId
);
httpBackend.flush().done(function() {done();});
}
it("Ali sends a message", function(done) {
bobUploadsKeys(function() {
aliDownloadsKeys(function() {
aliEnablesEncryption(function() {
aliSendsMessage(done);
});
});
});
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.catch(test_utils.failTest).nodeify(done);
});
function bobRecvMessage(done) {
var syncData = {
next_batch: "x",
rooms: {
join: {
}
}
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
utils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: aliMessage
})
]
}
};
httpBackend.when("GET", "/sync").respond(200, syncData);
bobClient.on("event", function(event) {
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toEqual({
msgtype: "m.text",
body: "Hello, World"
});
expect(event.isEncrypted()).toBeTruthy();
done();
});
bobClient.startClient();
httpBackend.flush();
}
it("Bob receives a message", function(done) {
bobUploadsKeys(function() {
aliDownloadsKeys(function() {
aliEnablesEncryption(function() {
aliSendsMessage(function() {
bobRecvMessage(done);
});
});
});
});
}, 30000); //timeout after 30s
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobStartClient)
.then(bobRecvMessage)
.catch(test_utils.failTest).done(done);
});
it("Ali blocks Bob's device", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliDownloadsKeys)
.then(function() {
aliClient.setDeviceBlocked(bobUserId, bobDeviceId, true);
var p1 = sendMessage(aliClient);
var p2 = expectAliQueryKeys()
.then(expectAliClaimKeys)
.then(function() {
return expectSendMessageRequest(aliHttpBackend);
}).then(function(sentContent) {
// no unblocked devices, so the ciphertext should be empty
expect(sentContent.ciphertext).toEqual({});
});
return q.all([p1, p2]);
}).catch(test_utils.failTest).nodeify(done);
});
it("Bob receives two pre-key messages", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobStartClient)
.then(bobRecvMessage)
.then(aliSendsMessage)
.then(bobRecvMessage)
.catch(test_utils.failTest).done(done);
});
it("Bob replies to the message", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobStartClient)
.then(bobRecvMessage)
.then(bobEnablesEncryption)
.then(bobSendsReplyMessage).then(function(ciphertext) {
expect(ciphertext.type).toEqual(1);
}).then(aliRecvMessage)
.catch(test_utils.failTest).done(done);
});
});
@@ -24,6 +24,7 @@ describe("MatrixClient events", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("emissions", function() {
+104 -19
View File
@@ -97,6 +97,7 @@ function startClient(httpBackend, client) {
describe("getEventTimeline support", function() {
var httpBackend;
var client;
beforeEach(function() {
utils.beforeEach(this);
@@ -104,8 +105,14 @@ describe("getEventTimeline support", function() {
sdk.request(httpBackend.requestFn);
});
afterEach(function() {
if (client) {
client.stopClient();
}
});
it("timeline support must be enabled to work", function(done) {
var client = sdk.createClient({
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
@@ -114,13 +121,14 @@ describe("getEventTimeline support", function() {
startClient(httpBackend, client
).then(function() {
var room = client.getRoom(roomId);
expect(function() { client.getEventTimeline(room, "event"); })
var timelineSet = room.getTimelineSets()[0];
expect(function() { client.getEventTimeline(timelineSet, "event"); })
.toThrow();
}).catch(utils.failTest).done(done);
});
it("timeline support works when enabled", function(done) {
var client = sdk.createClient({
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
@@ -130,7 +138,8 @@ describe("getEventTimeline support", function() {
startClient(httpBackend, client
).then(function() {
var room = client.getRoom(roomId);
expect(function() { client.getEventTimeline(room, "event"); })
var timelineSet = room.getTimelineSets()[0];
expect(function() { client.getEventTimeline(timelineSet, "event"); })
.not.toThrow();
}).catch(utils.failTest).done(done);
@@ -141,7 +150,7 @@ describe("getEventTimeline support", function() {
it("scrollback should be able to scroll back to before a gappy /sync",
function(done) {
// need a client with timelineSupport disabled to make this work
var client = sdk.createClient({
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
@@ -229,11 +238,13 @@ describe("MatrixClient event timelines", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("getEventTimeline", function() {
it("should create a new timeline for new events", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
.respond(200, function() {
return {
@@ -249,7 +260,7 @@ describe("MatrixClient event timelines", function() {
};
});
client.getEventTimeline(room, "event1:bar").then(function(tl) {
client.getEventTimeline(timelineSet, "event1:bar").then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
for (var i = 0; i < 4; i++) {
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
@@ -266,6 +277,7 @@ describe("MatrixClient event timelines", function() {
it("should return existing timeline for known events", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
@@ -283,7 +295,7 @@ describe("MatrixClient event timelines", function() {
});
httpBackend.flush("/sync").then(function() {
return client.getEventTimeline(room, EVENTS[0].event_id);
return client.getEventTimeline(timelineSet, EVENTS[0].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(EVENTS[0]);
@@ -297,6 +309,7 @@ describe("MatrixClient event timelines", function() {
it("should update timelines where they overlap a previous /sync", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
@@ -327,7 +340,7 @@ describe("MatrixClient event timelines", function() {
});
client.on("sync", function() {
client.getEventTimeline(room, EVENTS[2].event_id
client.getEventTimeline(timelineSet, EVENTS[2].event_id
).then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
@@ -346,6 +359,7 @@ describe("MatrixClient event timelines", function() {
it("should join timelines where they overlap a previous /context",
function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
// with context which joins them all up.
@@ -402,19 +416,19 @@ describe("MatrixClient event timelines", function() {
});
var tl0, tl2, tl3;
client.getEventTimeline(room, EVENTS[0].event_id
client.getEventTimeline(timelineSet, EVENTS[0].event_id
).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl0 = tl;
return client.getEventTimeline(room, EVENTS[2].event_id);
return client.getEventTimeline(timelineSet, EVENTS[2].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl2 = tl;
return client.getEventTimeline(room, EVENTS[3].event_id);
return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl3 = tl;
return client.getEventTimeline(room, EVENTS[1].event_id);
return client.getEventTimeline(timelineSet, EVENTS[1].event_id);
}).then(function(tl) {
// we expect it to get merged in with event 2
expect(tl.getEvents().length).toEqual(2);
@@ -439,6 +453,7 @@ describe("MatrixClient event timelines", function() {
it("should fail gracefully if there is no event field", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
// with context which joins them all up.
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1")
@@ -452,7 +467,7 @@ describe("MatrixClient event timelines", function() {
};
});
client.getEventTimeline(room, "event1"
client.getEventTimeline(timelineSet, "event1"
).then(function(tl) {
// could do with a fail()
expect(true).toBeFalsy();
@@ -467,6 +482,7 @@ describe("MatrixClient event timelines", function() {
describe("paginateEventTimeline", function() {
it("should allow you to paginate backwards", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
@@ -495,7 +511,7 @@ describe("MatrixClient event timelines", function() {
});
var tl;
client.getEventTimeline(room, EVENTS[0].event_id
client.getEventTimeline(timelineSet, EVENTS[0].event_id
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(tl, {backwards: true});
@@ -517,6 +533,7 @@ describe("MatrixClient event timelines", function() {
it("should allow you to paginate forwards", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
@@ -545,7 +562,7 @@ describe("MatrixClient event timelines", function() {
});
var tl;
client.getEventTimeline(room, EVENTS[0].event_id
client.getEventTimeline(timelineSet, EVENTS[0].event_id
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(
@@ -599,10 +616,11 @@ describe("MatrixClient event timelines", function() {
it("should work when /send returns before /sync", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(room, event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
// 2 because the initial sync contained an event
expect(tl.getEvents().length).toEqual(2);
@@ -611,7 +629,7 @@ describe("MatrixClient event timelines", function() {
// now let the sync complete, and check it again
return httpBackend.flush("/sync", 1);
}).then(function() {
return client.getEventTimeline(room, event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
@@ -622,13 +640,14 @@ describe("MatrixClient event timelines", function() {
it("should work when /send returns after /sync", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
// initiate the send, and set up checks to be done when it completes
// - but note that it won't complete until after the /sync does, below.
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
console.log("sendTextMessage completed");
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(room, event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (2)");
expect(tl.getEvents().length).toEqual(2);
@@ -636,7 +655,7 @@ describe("MatrixClient event timelines", function() {
}).catch(utils.failTest).done(done);
httpBackend.flush("/sync", 1).then(function() {
return client.getEventTimeline(room, event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (1)");
expect(tl.getEvents().length).toEqual(2);
@@ -647,4 +666,70 @@ describe("MatrixClient event timelines", function() {
}).catch(utils.failTest);
});
});
it("should handle gappy syncs after redactions", function(done) {
// https://github.com/vector-im/vector-web/issues/1389
// a state event, followed by a redaction thereof
var event = utils.mkMembership({
room: roomId, mship: "join", user: otherUserId
});
var redaction = utils.mkEvent({
type: "m.room.redaction",
room_id: roomId,
sender: otherUserId,
content: {}
});
redaction.redacts = event.event_id;
var syncData = {
next_batch: "batch1",
rooms: {
join: {},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
event,
redaction,
],
limited: false,
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.flush().then(function() {
var room = client.getRoom(roomId);
var tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[1].isRedacted()).toBe(true);
var sync2 = {
next_batch: "batch2",
rooms: {
join: {},
},
};
sync2.rooms.join[roomId] = {
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "world"
}),
],
limited: true,
prev_batch: "newerTok",
},
};
httpBackend.when("GET", "/sync").respond(200, sync2);
return httpBackend.flush();
}).then(function() {
var room = client.getRoom(roomId);
var tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(1);
}).catch(utils.failTest).done(done);
});
});
+110 -3
View File
@@ -6,10 +6,11 @@ var Room = publicGlobals.Room;
var MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
var Filter = publicGlobals.Filter;
var utils = require("../test-utils");
var MockStorageApi = require("../MockStorageApi");
describe("MatrixClient", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend, store;
var client, httpBackend, store, sessionStore;
var userId = "@alice:localhost";
var accessToken = "aseukfgwef";
@@ -17,12 +18,18 @@ describe("MatrixClient", function() {
utils.beforeEach(this);
httpBackend = new HttpBackend();
store = new MatrixInMemoryStore();
var mockStorage = new MockStorageApi();
sessionStore = new sdk.WebStorageSessionStore(mockStorage);
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
deviceId: "aliceDevice",
accessToken: accessToken,
store: store
store: store,
sessionStore: sessionStore,
});
});
@@ -34,7 +41,7 @@ describe("MatrixClient", function() {
it("should no-op if you've already joined a room", function() {
var roomId = "!foo:bar";
var room = new Room(roomId);
room.addEvents([
room.addLiveEvents([
utils.mkMembership({
user: userId, room: roomId, mship: "join", event: true
})
@@ -173,4 +180,104 @@ describe("MatrixClient", function() {
});
});
});
describe("downloadKeys", function() {
it("should do an HTTP request and then store the keys", function(done) {
var ed25519key = "wV5E3EUSHpHuoZLljNzojlabjGdXT3Mz7rugG9zgbkI";
var borisKeys = {
dev1: {
algorithms: ["1"], keys: { "ed25519:dev1": ed25519key },
signatures: {
boris: {
"ed25519:dev1":
"u99n8WZ61G//K6eVgYc+RDLVapmjttxqhjNucIFGEIJ" +
"oA4TUY8FmiGv3zl0EA71zrvPDfnFL5XLNsdc55NGbDg"
}
},
unsigned: { "abc": "def" },
}
};
var chazKeys = {
dev2: {
algorithms: ["2"], keys: { "ed25519:dev2": ed25519key },
signatures: {
chaz: {
"ed25519:dev2":
"8eaeXUWy9AQzjaNVOjVLs4FQk+cgobkNS811EjZBCMA" +
"apd8aPOfE26E13nFFOCLC1V6fOH5wVo61hxGR/j4PBA"
}
},
unsigned: { "ghi": "def" },
}
};
var daveKeys = {
dev3: {
algorithms: ["3"], keys: { "ed25519:dev2": ed25519key },
signatures: {
dave: {
"ed25519:dev2":
"8eaeXUWy9AQzjaNVOjVLs4FQk+cgobkNS811EjZBCMA" +
"apd8aPOfE26E13nFFOCLC1V6fOH5wVo61hxGR/j4PBA"
}
},
unsigned: { "ghi": "def" },
}
};
httpBackend.when("POST", "/keys/query").check(function(req) {
expect(req.data).toEqual({device_keys: {boris: {}, chaz: {}, dave: {}}});
}).respond(200, {
device_keys: {
boris: borisKeys,
chaz: chazKeys,
dave: daveKeys,
},
});
client.downloadKeys(["boris", "chaz", "dave"]).then(function(res) {
assertObjectContains(res.boris.dev1, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev1": ed25519key },
algorithms: ["1"],
unsigned: { "abc": "def" },
});
assertObjectContains(res.chaz.dev2, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev2" : ed25519key },
algorithms: ["2"],
unsigned: { "ghi": "def" },
});
// dave's key fails validation.
expect(res.dave).toEqual({});
}).catch(utils.failTest).done(done);
httpBackend.flush();
});
it("should return a rejected promise if the request fails", function(done) {
httpBackend.when("POST", "/keys/query").respond(400);
var exceptionThrown;
client.downloadKeys(["bottom"]).then(function() {
fail("download didn't fail");
}, function(err) {
exceptionThrown = err;
}).then(function() {
expect(exceptionThrown).toBeTruthy();
}).catch(utils.failTest).done(done);
httpBackend.flush();
});
});
});
function assertObjectContains(obj, expected) {
for (var k in expected) {
if (expected.hasOwnProperty(k)) {
expect(obj[k]).toEqual(expected[k]);
}
}
}
+4
View File
@@ -73,6 +73,10 @@ describe("MatrixClient opts", function() {
});
});
afterEach(function() {
client.stopClient();
});
it("should be able to send messages", function(done) {
var eventId = "$flibble:wibble";
httpBackend.when("PUT", "/txn1").respond(200, {
+14 -11
View File
@@ -66,20 +66,23 @@ describe("MatrixClient retrying", function() {
ev2 = tl[1];
expect(ev1.status).toEqual(EventStatus.SENDING);
expect(ev2.status).toEqual(EventStatus.QUEUED);
expect(ev2.status).toEqual(EventStatus.SENDING);
// now we can cancel the second and check everything looks sane
client.cancelPendingEvent(ev2);
expect(ev2.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(1);
// the first message should get sent, and the second should get queued
httpBackend.when("PUT", "/send/m.room.message/").check(function(rq) {
// ev2 should now have been queued
expect(ev2.status).toEqual(EventStatus.QUEUED);
// shouldn't be able to cancel the first message yet
expect(function() { client.cancelPendingEvent(ev1); })
.toThrow();
// now we can cancel the second and check everything looks sane
client.cancelPendingEvent(ev2);
expect(ev2.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(1);
// shouldn't be able to cancel the first message yet
expect(function() { client.cancelPendingEvent(ev1); })
.toThrow();
}).respond(400); // fail the first message
// fail the first send
httpBackend.when("PUT", "/send/m.room.message/")
.respond(400);
httpBackend.flush().then(function() {
expect(ev1.status).toEqual(EventStatus.NOT_SENT);
expect(tl.length).toEqual(1);
+10 -11
View File
@@ -126,6 +126,7 @@ describe("MatrixClient room timelines", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("local echo events", function() {
@@ -391,16 +392,16 @@ describe("MatrixClient room timelines", function() {
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
expect(index).toEqual(2);
expect(room.timeline[room.timeline.length - 1].event).toEqual(
expect(room.timeline.length).toEqual(3);
expect(room.timeline[2].event).toEqual(
eventData[1]
);
expect(room.timeline[room.timeline.length - 2].event).toEqual(
expect(room.timeline[1].event).toEqual(
eventData[0]
);
done();
});
}).catch(utils.failTest).done(done);
});
httpBackend.flush("/sync", 1);
});
@@ -419,13 +420,12 @@ describe("MatrixClient room timelines", function() {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
httpBackend.flush("/sync", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
var preNameEvent = room.timeline[room.timeline.length - 3];
var postNameEvent = room.timeline[room.timeline.length - 1];
expect(preNameEvent.sender.name).toEqual(userName);
expect(postNameEvent.sender.name).toEqual("New Name");
done();
});
}).catch(utils.failTest).done(done);
});
httpBackend.flush("/sync", 1);
});
@@ -487,7 +487,7 @@ describe("MatrixClient room timelines", function() {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
httpBackend.flush("/sync", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC).name).toEqual("C");
expect(room.currentState.getMember(userC).membership).toEqual(
@@ -497,8 +497,7 @@ describe("MatrixClient room timelines", function() {
expect(room.currentState.getMember(userD).membership).toEqual(
"invite"
);
done();
});
}).catch(utils.failTest).done(done);
});
httpBackend.flush("/sync", 1);
});
+1
View File
@@ -32,6 +32,7 @@ describe("MatrixClient syncing", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("startClient", function() {
+14
View File
@@ -15,6 +15,20 @@ function HttpBackend() {
realReq.callback = callback;
console.log("HTTP backend received request: %s %s", opts.method, opts.uri);
self.requests.push(realReq);
var abort = function() {
var idx = self.requests.indexOf(realReq);
if (idx >= 0) {
console.log("Aborting HTTP request: %s %s", opts.method,
opts.uri);
self.requests.splice(idx, 1);
realReq.callback("aborted");
}
};
return {
abort: abort
};
};
}
HttpBackend.prototype = {
+6 -6
View File
@@ -48,7 +48,7 @@ module.exports.mock = function(constr, name) {
* @param {Object} opts Values for the event.
* @param {string} opts.type The event.type
* @param {string} opts.room The event.room_id
* @param {string} opts.user The event.user_id
* @param {string} opts.sender The event.sender
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
* @param {Object} opts.content The event.content
* @param {boolean} opts.event True to make a MatrixEvent.
@@ -61,7 +61,7 @@ module.exports.mkEvent = function(opts) {
var event = {
type: opts.type,
room_id: opts.room,
sender: opts.user,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
event_id: "$" + Math.random() + "-" + Math.random()
};
@@ -88,7 +88,7 @@ module.exports.mkPresence = function(opts) {
var event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
sender: opts.user,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: {
avatar_url: opts.url,
displayname: opts.name,
@@ -104,8 +104,8 @@ module.exports.mkPresence = function(opts) {
* @param {Object} opts Values for the membership.
* @param {string} opts.room The room ID for the event.
* @param {string} opts.mship The content.membership for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.skey The other user ID for the event if applicable
* @param {string} opts.sender The sender user ID for the event.
* @param {string} opts.skey The target user ID for the event if applicable
* e.g. for invites/bans.
* @param {string} opts.name The content.displayname for the event.
* @param {string} opts.url The content.avatar_url for the event.
@@ -115,7 +115,7 @@ module.exports.mkPresence = function(opts) {
module.exports.mkMembership = function(opts) {
opts.type = "m.room.member";
if (!opts.skey) {
opts.skey = opts.user;
opts.skey = opts.sender || opts.user;
}
if (!opts.mship) {
throw new Error("Missing .mship => " + JSON.stringify(opts));
+14
View File
@@ -0,0 +1,14 @@
"use strict";
var Crypto = require("../../lib/crypto");
var sdk = require("../..");
describe("Crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
it("Crypto exposes the correct olm library version", function() {
expect(Crypto.getOlmVersion()).toEqual([1, 3, 0]);
});
});
+6 -1
View File
@@ -16,7 +16,12 @@ describe("EventTimeline", function() {
beforeEach(function() {
utils.beforeEach(this);
timeline = new EventTimeline(roomId);
// XXX: this is a horrid hack; should use sinon or something instead to mock
var timelineSet = { room: { roomId: roomId }};
timelineSet.room.getUnfilteredTimelineSet = function() { return timelineSet; };
timeline = new EventTimeline(timelineSet);
});
describe("construction", function() {
+15 -15
View File
@@ -214,25 +214,25 @@ describe('NotificationService', function() {
it('should bing on a user ID.', function() {
testEvent.event.content.body = "Hello @ali:matrix.org, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent.event);
var actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a partial user ID with an @.', function() {
testEvent.event.content.body = "Hello @ali, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent.event);
var actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a partial user ID without @.', function() {
testEvent.event.content.body = "Hello ali, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent.event);
var actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a case-insensitive user ID.', function() {
testEvent.event.content.body = "Hello @AlI:matrix.org, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent.event);
var actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
@@ -240,13 +240,13 @@ describe('NotificationService', function() {
it('should bing on a display name.', function() {
testEvent.event.content.body = "Hello Alice M, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent.event);
var actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a case-insensitive display name.', function() {
testEvent.event.content.body = "Hello ALICE M, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent.event);
var actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
@@ -254,43 +254,43 @@ describe('NotificationService', function() {
it('should bing on a bing word.', function() {
testEvent.event.content.body = "I really like coffee";
var actions = pushProcessor.actionsForEvent(testEvent.event);
var actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on case-insensitive bing words.', function() {
testEvent.event.content.body = "Coffee is great";
var actions = pushProcessor.actionsForEvent(testEvent.event);
var actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on wildcard (.*) bing words.', function() {
testEvent.event.content.body = "It was foomahbar I think.";
var actions = pushProcessor.actionsForEvent(testEvent.event);
var actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on character group ([abc]) bing words.', function() {
testEvent.event.content.body = "Ping!";
var actions = pushProcessor.actionsForEvent(testEvent.event);
var actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
testEvent.event.content.body = "Pong!";
actions = pushProcessor.actionsForEvent(testEvent.event);
actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on character range ([a-z]) bing words.', function() {
testEvent.event.content.body = "I ate 6 pies";
var actions = pushProcessor.actionsForEvent(testEvent.event);
var actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on character negation ([!a]) bing words.', function() {
testEvent.event.content.body = "boke";
var actions = pushProcessor.actionsForEvent(testEvent.event);
var actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
testEvent.event.content.body = "bake";
actions = pushProcessor.actionsForEvent(testEvent.event);
actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(false);
});
@@ -298,7 +298,7 @@ describe('NotificationService', function() {
it('should gracefully handle bad input.', function() {
testEvent.event.content.body = { "foo": "bar" };
var actions = pushProcessor.actionsForEvent(testEvent.event);
var actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(false);
});
});
+179
View File
@@ -0,0 +1,179 @@
"use strict";
var callbacks = require("../../lib/realtime-callbacks");
var test_utils = require("../test-utils.js");
describe("realtime-callbacks", function() {
var clock = jasmine.Clock;
var fakeDate;
function tick(millis) {
// make sure we tick the fakedate first, otherwise nothing will happen!
fakeDate += millis;
clock.tick(millis);
}
beforeEach(function() {
test_utils.beforeEach(this);
clock.useMock();
fakeDate = Date.now();
callbacks.setNow(function() { return fakeDate; });
});
afterEach(function() {
callbacks.setNow();
});
describe("setTimeout", function() {
it("should call the callback after the timeout", function() {
var callback = jasmine.createSpy();
callbacks.setTimeout(callback, 100);
expect(callback).not.toHaveBeenCalled();
tick(100);
expect(callback).toHaveBeenCalled();
});
it("should default to a zero timeout", function() {
var callback = jasmine.createSpy();
callbacks.setTimeout(callback);
expect(callback).not.toHaveBeenCalled();
tick(0);
expect(callback).toHaveBeenCalled();
});
it("should pass any parameters to the callback", function() {
var callback = jasmine.createSpy();
callbacks.setTimeout(callback, 0, "a", "b", "c");
tick(0);
expect(callback).toHaveBeenCalledWith("a", "b", "c");
});
it("should set 'this' to the global object", function() {
var callback = jasmine.createSpy();
callback.andCallFake(function() {
expect(this).toBe(global);
expect(this.console).toBeDefined();
});
callbacks.setTimeout(callback);
tick(0);
expect(callback).toHaveBeenCalled();
});
it("should handle timeouts of several seconds", function() {
var callback = jasmine.createSpy();
callbacks.setTimeout(callback, 2000);
expect(callback).not.toHaveBeenCalled();
for (var i = 0; i < 4; i++) {
tick(500);
}
expect(callback).toHaveBeenCalled();
});
it("should call multiple callbacks in the right order", function() {
var callback1 = jasmine.createSpy("callback1");
var callback2 = jasmine.createSpy("callback2");
var callback3 = jasmine.createSpy("callback3");
callbacks.setTimeout(callback2, 200);
callbacks.setTimeout(callback1, 100);
callbacks.setTimeout(callback3, 300);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
expect(callback3).toHaveBeenCalled();
});
it("should treat -ve timeouts the same as a zero timeout", function() {
var callback1 = jasmine.createSpy("callback1");
var callback2 = jasmine.createSpy("callback2");
// check that cb1 is called before cb2
callback1.andCallFake(function() {
expect(callback2).not.toHaveBeenCalled();
});
callbacks.setTimeout(callback1);
callbacks.setTimeout(callback2, -100);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
it("should not get confused by chained calls", function() {
var callback2 = jasmine.createSpy("callback2");
var callback1 = jasmine.createSpy("callback1");
callback1.andCallFake(function() {
callbacks.setTimeout(callback2, 0);
expect(callback2).not.toHaveBeenCalled();
});
callbacks.setTimeout(callback1);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
it("should be immune to exceptions", function() {
var callback1 = jasmine.createSpy("callback1");
callback1.andCallFake(function() {
throw new Error("prepare to die");
});
var callback2 = jasmine.createSpy("callback2");
callbacks.setTimeout(callback1, 0);
callbacks.setTimeout(callback2, 0);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
});
describe("cancelTimeout", function() {
it("should cancel a pending timeout", function() {
var callback = jasmine.createSpy();
var k = callbacks.setTimeout(callback);
callbacks.clearTimeout(k);
tick(0);
expect(callback).not.toHaveBeenCalled();
});
it("should not affect sooner timeouts", function() {
var callback1 = jasmine.createSpy("callback1");
var callback2 = jasmine.createSpy("callback2");
callbacks.setTimeout(callback1, 100);
var k = callbacks.setTimeout(callback2, 200);
callbacks.clearTimeout(k);
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
tick(150);
expect(callback2).not.toHaveBeenCalled();
});
});
});
+31
View File
@@ -102,6 +102,37 @@ describe("RoomMember", function() {
member.setPowerLevelEvent(event); // no-op
expect(emitCount).toEqual(1);
});
it("should honour power levels of zero.",
function() {
var event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@alice:bar": 0,
}
},
event: true
});
var emitCount = 0;
// set the power level to something other than zero or we
// won't get an event
member.powerLevel = 1;
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember.userId).toEqual('@alice:bar');
expect(emitMember.powerLevel).toEqual(0);
expect(emitEvent).toEqual(event);
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(0);
expect(emitCount).toEqual(1);
});
});
describe("setTypingEvent", function() {
+191 -189
View File
@@ -82,7 +82,7 @@ describe("Room", function() {
});
});
describe("addEvents", function() {
describe("addLiveEvents", function() {
var events = [
utils.mkMessage({
room: roomId, user: userA, msg: "changing room name", event: true
@@ -100,12 +100,12 @@ describe("Room", function() {
user_ids: [userA]
}
});
room.addEvents([typing]);
room.addLiveEvents([typing]);
expect(room.currentState.setTypingEvent).toHaveBeenCalledWith(typing);
});
it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", function() {
expect(function() { room.addEvents(events, "foo"); }).toThrow();
expect(function() { room.addLiveEvents(events, "foo"); }).toThrow();
});
it("should replace a timeline event if dupe strategy is 'replace'", function() {
@@ -114,9 +114,9 @@ describe("Room", function() {
room: roomId, user: userA, msg: "dupe", event: true
});
dupe.event.event_id = events[0].getId();
room.addEvents(events);
room.addLiveEvents(events);
expect(room.timeline[0]).toEqual(events[0]);
room.addEvents([dupe], "replace");
room.addLiveEvents([dupe], "replace");
expect(room.timeline[0]).toEqual(dupe);
});
@@ -126,39 +126,13 @@ describe("Room", function() {
room: roomId, user: userA, msg: "dupe", event: true
});
dupe.event.event_id = events[0].getId();
room.addEvents(events);
room.addLiveEvents(events);
expect(room.timeline[0]).toEqual(events[0]);
room.addEvents([dupe], "ignore");
room.addLiveEvents([dupe], "ignore");
expect(room.timeline[0]).toEqual(events[0]);
});
});
describe("addEventsToTimeline", function() {
var events = [
utils.mkMessage({
room: roomId, user: userA, msg: "changing room name", event: true
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" }
})
];
it("should be able to add events to the end", function() {
room.addEventsToTimeline(events);
expect(room.timeline.length).toEqual(2);
expect(room.timeline[0]).toEqual(events[0]);
expect(room.timeline[1]).toEqual(events[1]);
});
it("should be able to add events to the start", function() {
room.addEventsToTimeline(events, true);
expect(room.timeline.length).toEqual(2);
expect(room.timeline[0]).toEqual(events[1]);
expect(room.timeline[1]).toEqual(events[0]);
});
it("should emit 'Room.timeline' events when added to the end",
it("should emit 'Room.timeline' events",
function() {
var callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) {
@@ -168,99 +142,10 @@ describe("Room", function() {
expect(emitRoom).toEqual(room);
expect(toStart).toBeFalsy();
});
room.addEventsToTimeline(events);
room.addLiveEvents(events);
expect(callCount).toEqual(2);
});
it("should emit 'Room.timeline' events when added to the start",
function() {
var callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) {
callCount += 1;
expect(room.timeline.length).toEqual(callCount);
expect(event).toEqual(events[callCount - 1]);
expect(emitRoom).toEqual(room);
expect(toStart).toBe(true);
});
room.addEventsToTimeline(events, true);
expect(callCount).toEqual(2);
});
it("should set event.sender for new and old events", function() {
var sentinel = {
userId: userA,
membership: "join",
name: "Alice"
};
var oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
};
room.currentState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" }
});
var oldEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "Old Room Name" }
});
room.addEventsToTimeline([newEv]);
expect(newEv.sender).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true);
expect(oldEv.sender).toEqual(oldSentinel);
});
it("should set event.target for new and old m.room.member events",
function() {
var sentinel = {
userId: userA,
membership: "join",
name: "Alice"
};
var oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
};
room.currentState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
});
var oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true
});
room.addEventsToTimeline([newEv]);
expect(newEv.target).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true);
expect(oldEv.target).toEqual(oldSentinel);
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for new events", function() {
var events = [
@@ -274,7 +159,7 @@ describe("Room", function() {
}
})
];
room.addEventsToTimeline(events);
room.addLiveEvents(events);
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
[events[0]]
);
@@ -286,33 +171,6 @@ describe("Room", function() {
expect(room.oldState.setStateEvents).not.toHaveBeenCalled();
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for old events", function() {
var events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room"
}
})
];
room.addEventsToTimeline(events, true);
expect(room.oldState.setStateEvents).toHaveBeenCalledWith(
[events[0]]
);
expect(room.oldState.setStateEvents).toHaveBeenCalledWith(
[events[1]]
);
expect(events[0].forwardLooking).toBe(false);
expect(events[1].forwardLooking).toBe(false);
expect(room.currentState.setStateEvents).not.toHaveBeenCalled();
});
it("should synthesize read receipts for the senders of events", function() {
var sentinel = {
userId: userA,
@@ -325,7 +183,7 @@ describe("Room", function() {
}
return null;
});
room.addEventsToTimeline(events);
room.addLiveEvents(events);
expect(room.getEventReadUpTo(userA)).toEqual(events[1].getId());
});
@@ -370,13 +228,155 @@ describe("Room", function() {
expect(room.timeline.length).toEqual(1);
// then the remoteEvent
room.addEventsToTimeline([remoteEvent]);
room.addLiveEvents([remoteEvent]);
expect(room.timeline.length).toEqual(1);
expect(callCount).toEqual(2);
});
});
describe("addEventsToTimeline", function() {
var events = [
utils.mkMessage({
room: roomId, user: userA, msg: "changing room name", event: true
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" }
})
];
it("should not be able to add events to the end", function() {
expect(function() {
room.addEventsToTimeline(events, false, room.getLiveTimeline());
}).toThrow();
});
it("should be able to add events to the start", function() {
room.addEventsToTimeline(events, true, room.getLiveTimeline());
expect(room.timeline.length).toEqual(2);
expect(room.timeline[0]).toEqual(events[1]);
expect(room.timeline[1]).toEqual(events[0]);
});
it("should emit 'Room.timeline' events when added to the start",
function() {
var callCount = 0;
room.on("Room.timeline", function(event, emitRoom, toStart) {
callCount += 1;
expect(room.timeline.length).toEqual(callCount);
expect(event).toEqual(events[callCount - 1]);
expect(emitRoom).toEqual(room);
expect(toStart).toBe(true);
});
room.addEventsToTimeline(events, true, room.getLiveTimeline());
expect(callCount).toEqual(2);
});
});
describe("event metadata handling", function() {
it("should set event.sender for new and old events", function() {
var sentinel = {
userId: userA,
membership: "join",
name: "Alice"
};
var oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
};
room.currentState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" }
});
var oldEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "Old Room Name" }
});
room.addLiveEvents([newEv]);
expect(newEv.sender).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true, room.getLiveTimeline());
expect(oldEv.sender).toEqual(oldSentinel);
});
it("should set event.target for new and old m.room.member events",
function() {
var sentinel = {
userId: userA,
membership: "join",
name: "Alice"
};
var oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
};
room.currentState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
room.oldState.getSentinelMember.andCallFake(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
});
var oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true
});
room.addLiveEvents([newEv]);
expect(newEv.target).toEqual(sentinel);
room.addEventsToTimeline([oldEv], true, room.getLiveTimeline());
expect(oldEv.target).toEqual(oldSentinel);
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for old events", function() {
var events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room"
}
})
];
room.addEventsToTimeline(events, true, room.getLiveTimeline());
expect(room.oldState.setStateEvents).toHaveBeenCalledWith(
[events[0]]
);
expect(room.oldState.setStateEvents).toHaveBeenCalledWith(
[events[1]]
);
expect(events[0].forwardLooking).toBe(false);
expect(events[1].forwardLooking).toBe(false);
expect(room.currentState.setStateEvents).not.toHaveBeenCalled();
});
});
var resetTimelineTests = function(timelineSupport) {
var events = [
utils.mkMessage({
@@ -397,11 +397,11 @@ describe("Room", function() {
});
it("should copy state from previous timeline", function() {
room.addEventsToTimeline([events[0], events[1]]);
room.addLiveEvents([events[0], events[1]]);
expect(room.getLiveTimeline().getEvents().length).toEqual(2);
room.resetLiveTimeline();
room.addEventsToTimeline([events[2]]);
room.addLiveEvents([events[2]]);
var oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS);
var newState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
expect(room.getLiveTimeline().getEvents().length).toEqual(1);
@@ -410,11 +410,11 @@ describe("Room", function() {
});
it("should reset the legacy timeline fields", function() {
room.addEventsToTimeline([events[0], events[1]]);
room.addLiveEvents([events[0], events[1]]);
expect(room.timeline.length).toEqual(2);
room.resetLiveTimeline();
room.addEventsToTimeline([events[2]]);
room.addLiveEvents([events[2]]);
var newLiveTimeline = room.getLiveTimeline();
expect(room.timeline).toEqual(newLiveTimeline.getEvents());
expect(room.oldState).toEqual(
@@ -443,7 +443,7 @@ describe("Room", function() {
it("should " + (timelineSupport ? "remember" : "forget") +
" old timelines", function() {
room.addEventsToTimeline([events[0]]);
room.addLiveEvents([events[0]]);
expect(room.timeline.length).toEqual(1);
var firstLiveTimeline = room.getLiveTimeline();
room.resetLiveTimeline();
@@ -477,16 +477,16 @@ describe("Room", function() {
];
it("should handle events in the same timeline", function() {
room.addEventsToTimeline(events);
room.addLiveEvents(events);
expect(room.compareEventOrdering(events[0].getId(),
events[1].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
events[1].getId()))
.toBeLessThan(0);
expect(room.compareEventOrdering(events[2].getId(),
events[1].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId(),
events[1].getId()))
.toBeGreaterThan(0);
expect(room.compareEventOrdering(events[1].getId(),
events[1].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
events[1].getId()))
.toEqual(0);
});
@@ -496,13 +496,13 @@ describe("Room", function() {
room.getLiveTimeline().setNeighbouringTimeline(oldTimeline, 'b');
room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addEventsToTimeline([events[1]]);
room.addLiveEvents([events[1]]);
expect(room.compareEventOrdering(events[0].getId(),
events[1].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
events[1].getId()))
.toBeLessThan(0);
expect(room.compareEventOrdering(events[1].getId(),
events[0].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
events[0].getId()))
.toBeGreaterThan(0);
});
@@ -510,26 +510,28 @@ describe("Room", function() {
var oldTimeline = room.addTimeline();
room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addEventsToTimeline([events[1]]);
room.addLiveEvents([events[1]]);
expect(room.compareEventOrdering(events[0].getId(),
events[1].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
events[1].getId()))
.toBe(null);
expect(room.compareEventOrdering(events[1].getId(),
events[0].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
events[0].getId()))
.toBe(null);
});
it("should return null for unknown events", function() {
room.addEventsToTimeline(events);
room.addLiveEvents(events);
expect(room.compareEventOrdering(events[0].getId(), "xxx"))
.toBe(null);
expect(room.compareEventOrdering("xxx", events[0].getId()))
.toBe(null);
expect(room.compareEventOrdering(events[0].getId(),
events[0].getId()))
.toBe(0);
expect(room.getUnfilteredTimelineSet()
.compareEventOrdering(events[0].getId(), "xxx"))
.toBe(null);
expect(room.getUnfilteredTimelineSet()
.compareEventOrdering("xxx", events[0].getId()))
.toBe(null);
expect(room.getUnfilteredTimelineSet()
.compareEventOrdering(events[0].getId(), events[0].getId()))
.toBe(0);
});
});
@@ -1068,7 +1070,7 @@ describe("Room", function() {
}),
];
room.addEventsToTimeline(events);
room.addLiveEvents(events);
var ts = 13787898424;
// check it initialises correctly
@@ -1159,9 +1161,9 @@ describe("Room", function() {
var eventC = utils.mkMessage({
room: roomId, user: userA, msg: "remote 2", event: true
});
room.addEvents([eventA]);
room.addLiveEvents([eventA]);
room.addPendingEvent(eventB, "TXN1");
room.addEvents([eventC]);
room.addLiveEvents([eventC]);
expect(room.timeline).toEqual(
[eventA, eventC]
);
@@ -1185,9 +1187,9 @@ describe("Room", function() {
var eventC = utils.mkMessage({
room: roomId, user: userA, msg: "remote 2", event: true
});
room.addEvents([eventA]);
room.addLiveEvents([eventA]);
room.addPendingEvent(eventB, "TXN1");
room.addEvents([eventC]);
room.addLiveEvents([eventC]);
expect(room.timeline).toEqual(
[eventA, eventB, eventC]
);
+56 -14
View File
@@ -18,7 +18,11 @@ function createTimeline(numEvents, baseIndex) {
if (numEvents === undefined) { numEvents = 3; }
if (baseIndex === undefined) { baseIndex = 1; }
var timeline = new EventTimeline(ROOM_ID);
// XXX: this is a horrid hack
var timelineSet = { room: { roomId: ROOM_ID }};
timelineSet.room.getUnfilteredTimelineSet = function() { return timelineSet; };
var timeline = new EventTimeline(timelineSet);
// add the events after the baseIndex first
addEventsToTimeline(timeline, numEvents - baseIndex, false);
@@ -133,19 +137,19 @@ describe("TimelineIndex", function() {
describe("TimelineWindow", function() {
/**
* create a dummy room and client, and a TimelineWindow
* create a dummy eventTimelineSet and client, and a TimelineWindow
* attached to them.
*/
var room, client;
var timelineSet, client;
function createWindow(timeline, opts) {
room = {};
timelineSet = {};
client = {};
client.getEventTimeline = function(room0, eventId0) {
expect(room0).toBe(room);
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineSet0).toBe(timelineSet);
return q(timeline);
};
return new TimelineWindow(client, room, opts);
return new TimelineWindow(client, timelineSet, opts);
}
beforeEach(function() {
@@ -169,15 +173,15 @@ describe("TimelineWindow", function() {
var timeline = createTimeline();
var eventId = timeline.getEvents()[1].getId();
var room = {};
var timelineSet = {};
var client = {};
client.getEventTimeline = function(room0, eventId0) {
expect(room0).toBe(room);
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineSet0).toBe(timelineSet);
expect(eventId0).toEqual(eventId);
return q(timeline);
};
var timelineWindow = new TimelineWindow(client, room);
var timelineWindow = new TimelineWindow(client, timelineSet);
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
@@ -192,12 +196,12 @@ describe("TimelineWindow", function() {
var eventId = timeline.getEvents()[1].getId();
var room = {};
var timelineSet = {};
var client = {};
var timelineWindow = new TimelineWindow(client, room);
var timelineWindow = new TimelineWindow(client, timelineSet);
client.getEventTimeline = function(room0, eventId0) {
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
@@ -418,5 +422,43 @@ describe("TimelineWindow", function() {
}).catch(utils.failTest).done(done);
});
it("should limit the number of unsuccessful pagination requests",
function(done) {
var timeline = createTimeline();
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
var timelineWindow = createWindow(timeline, {windowLimit: 5});
var eventId = timeline.getEvents()[1].getId();
var paginateCount = 0;
client.paginateEventTimeline = function(timeline0, opts) {
expect(timeline0).toBe(timeline);
expect(opts.backwards).toBe(false);
expect(opts.limit).toEqual(2);
paginateCount += 1;
return q(true);
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2, true, 3);
}).then(function(success) {
expect(success).toBe(false);
expect(paginateCount).toEqual(3);
var expectedEvents = timeline.getEvents().slice(0, 3);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
}).catch(utils.failTest).done(done);
});
});
});
+128
View File
@@ -132,4 +132,132 @@ describe("utils", function() {
}, ["foo"]); }).not.toThrow();
});
});
describe("deepCompare", function() {
var assert = {
isTrue: function(x) { expect(x).toBe(true); },
isFalse: function(x) { expect(x).toBe(false); },
};
it("should handle primitives", function() {
assert.isTrue(utils.deepCompare(null, null));
assert.isFalse(utils.deepCompare(null, undefined));
assert.isTrue(utils.deepCompare("hi", "hi"));
assert.isTrue(utils.deepCompare(5, 5));
assert.isFalse(utils.deepCompare(5, 10));
});
it("should handle regexps", function() {
assert.isTrue(utils.deepCompare(/abc/, /abc/));
assert.isFalse(utils.deepCompare(/abc/, /123/));
var r = /abc/;
assert.isTrue(utils.deepCompare(r, r));
});
it("should handle dates", function() {
assert.isTrue(utils.deepCompare(new Date("2011-03-31"),
new Date("2011-03-31")));
assert.isFalse(utils.deepCompare(new Date("2011-03-31"),
new Date("1970-01-01")));
});
it("should handle arrays", function() {
assert.isTrue(utils.deepCompare([], []));
assert.isTrue(utils.deepCompare([1, 2], [1, 2]));
assert.isFalse(utils.deepCompare([1, 2], [2, 1]));
assert.isFalse(utils.deepCompare([1, 2], [1, 2, 3]));
});
it("should handle simple objects", function() {
assert.isTrue(utils.deepCompare({}, {}));
assert.isTrue(utils.deepCompare({a: 1, b: 2}, {a: 1, b: 2}));
assert.isTrue(utils.deepCompare({a: 1, b: 2}, {b: 2, a: 1}));
assert.isFalse(utils.deepCompare({a: 1, b: 2}, {a: 1, b: 3}));
assert.isTrue(utils.deepCompare({1: {name: "mhc", age: 28},
2: {name: "arb", age: 26}},
{1: {name: "mhc", age: 28},
2: {name: "arb", age: 26}}));
assert.isFalse(utils.deepCompare({1: {name: "mhc", age: 28},
2: {name: "arb", age: 26}},
{1: {name: "mhc", age: 28},
2: {name: "arb", age: 27}}));
assert.isFalse(utils.deepCompare({}, null));
assert.isFalse(utils.deepCompare({}, undefined));
});
it("should handle functions", function() {
// no two different function is equal really, they capture their
// context variables so even if they have same toString(), they
// won't have same functionality
var func = function(x) { return true; };
var func2 = function(x) { return true; };
assert.isTrue(utils.deepCompare(func, func));
assert.isFalse(utils.deepCompare(func, func2));
assert.isTrue(utils.deepCompare({ a: { b: func } }, { a: { b: func } }));
assert.isFalse(utils.deepCompare({ a: { b: func } }, { a: { b: func2 } }));
});
});
describe("extend", function() {
var SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
it("should extend", function() {
var target = {
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
};
var merged = {
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
"newprop": "new",
};
var source_orig = JSON.stringify(SOURCE);
utils.extend(target, SOURCE);
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
// check the originial wasn't modified
expect(JSON.stringify(SOURCE)).toEqual(source_orig);
});
it("should ignore null", function() {
var target = {
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
};
var merged = {
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
"newprop": "new",
};
var source_orig = JSON.stringify(SOURCE);
utils.extend(target, null, SOURCE);
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
// check the originial wasn't modified
expect(JSON.stringify(SOURCE)).toEqual(source_orig);
});
it("should handle properties created with defineProperties", function() {
var source = Object.defineProperties({}, {
"enumerableProp": {
get: function() {
return true;
},
enumerable: true
},
"nonenumerableProp": {
get: function() {
return true;
}
}
});
var target = {};
utils.extend(target, source);
expect(target.enumerableProp).toBe(true);
expect(target.nonenumerableProp).toBe(undefined);
});
});
});
+1 -30
View File
@@ -5,36 +5,7 @@ var Room = sdk.Room;
var User = sdk.User;
var utils = require("../test-utils");
function MockStorageApi() {
this.data = {};
this.keys = [];
this.length = 0;
}
MockStorageApi.prototype = {
setItem: function(k, v) {
this.data[k] = v;
this._recalc();
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
this._recalc();
},
key: function(index) {
return this.keys[index];
},
_recalc: function() {
var keys = [];
for (var k in this.data) {
if (!this.data.hasOwnProperty(k)) { continue; }
keys.push(k);
}
this.keys = keys;
this.length = keys.length;
}
};
var MockStorageApi = require("../MockStorageApi");
describe("WebStorageStore", function() {
var store, room;