Compare commits

...

1081 Commits

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

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

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

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

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

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

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

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

Also:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

While we're in the area:

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

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

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

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

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

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

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

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

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

We should only be adding the raw event.

Fixes (hopefully) https://github.com/vector-im/vector-web/issues/1389.
2016-08-30 14:30:12 +01:00
Matthew Hodgson e18b446190 unbreak filter text 2016-08-30 01:30:23 +01:00
Matthew Hodgson 0848d4ed10 reemit Room.timeline events correctly 2016-08-30 01:13:32 +01:00
Matthew Hodgson 7514aea813 make most things work other than Room.timeline firing 2016-08-30 01:11:47 +01:00
Matthew Hodgson 58031ab21d fix things until they almost work again... 2016-08-30 00:36:52 +01:00
Matthew Hodgson c1c2ca3ec1 tweak doc; make it build 2016-08-29 23:18:19 +01:00
Matthew Hodgson b863a363da WIP refactor 2016-08-29 21:08:35 +01:00
Matthew Hodgson b42db46abd WIP refactor 2016-08-29 21:06:53 +01:00
Matthew Hodgson d46863e199 fix syntax 2016-08-28 23:44:10 +01:00
Matthew Hodgson 751ce421cd Merge branch 'develop' into matthew/filtered-timelines 2016-08-28 18:49:54 +01:00
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
Richard van der Hoff bbe41aa7b4 0.5.1 2016-03-30 13:18:42 +01:00
Richard van der Hoff ece9391878 Prepare changelog for v0.5.1 2016-03-30 13:17:10 +01:00
David Baker 64640287cf Merge pull request #119 from matrix-org/dbkr/member_count_only_joined
Only count joined members for the member count in notifications.
2016-03-24 14:01:45 +00:00
David Baker 57f88b00ba d'oh, no es6 in js-sdk 2016-03-24 13:57:55 +00:00
David Baker e618ad9589 Only count joined members for the member count in notifications. Fixes https://github.com/vector-im/vector-web/issues/1067 2016-03-24 13:55:30 +00:00
Richard van der Hoff 69c4d7b66e Merge pull request #118 from matrix-org/dbkr/maysendevent
Add maySendEvent to match maySendStateEvent
2016-03-24 11:43:29 +00:00
David Baker d7b3b91eec Failed to remove extra param 2016-03-23 17:08:22 +00:00
David Baker 88cc63e2a2 Add maySendEvent to match maySendStateEvent. Make them use the same function internally. Also add convenience maySendMessage. Also tests. 2016-03-23 15:10:51 +00:00
Richard van der Hoff cdea96fa0a Release script tweaks
- be more helpful if update_changelog is not installed
- behave sanely if v is omitted on tag arg
2016-03-23 14:50:03 +00:00
Matthew Hodgson cfc10fa82d fix invite picker info again... 2016-03-23 12:03:56 +00:00
Richard van der Hoff 9d8973bf1f 0.5.0 2016-03-22 17:56:32 +00:00
Richard van der Hoff 7f95237e02 Prepare changelog for v0.5.0 2016-03-22 17:56:05 +00:00
Matthew Hodgson e1415d9829 Merge pull request #117 from matrix-org/matthew/roomlist
get/setRoomVisibility API
2016-03-22 12:30:18 +00:00
Richard van der Hoff 19a12b3c79 Merge pull request #115 from matrix-org/rav/txnid_clashes
Include a counter in generated transaction IDs
2016-03-22 12:20:02 +00:00
Matthew Hodgson bec41e4f94 incorporate review 2016-03-22 10:20:02 +00:00
Matthew Hodgson 5f177aeec4 get/setRoomVisibility API 2016-03-22 00:55:53 +00:00
Matthew Hodgson b422916452 add to existing users if present, to avoid destroying presence data 2016-03-21 18:15:55 +00:00
Matthew Hodgson 10378c0e7f Merge pull request #116 from matrix-org/matthew/fix-invite-picker-info
update store user metadata based on membership events rather than presence
2016-03-21 16:12:42 +00:00
Matthew Hodgson fba4d5fb0a Merge pull request #114 from matrix-org/matthew/stop-peeking
API to stop peeking
2016-03-21 16:12:29 +00:00
Matthew Hodgson 77101823f5 track kicked rooms correctly 2016-03-19 02:18:37 +00:00
Matthew Hodgson 15bc608368 presence no longer returns profile data, so we have to update our store's users based on membership events instead 2016-03-19 01:45:10 +00:00
Richard van der Hoff dfc4b34d09 Include a counter in generated transaction IDs
Fixes a flaky test which sometimes failed due to sending two events in the same
millisecond.
2016-03-18 21:32:15 +00:00
Richard van der Hoff ad9daecbd4 Pass the right options into SyncApi when peeking
When we peek into a room, we create its Room object. We need to make sure it is
created with the same options as we would if it were created via the /sync
calls.

Save the options passed in when startClient is called, and then pass them into
the SyncApi each time we create it.
2016-03-18 20:59:23 +00:00
Matthew Hodgson d29302716d oops 2016-03-18 19:25:22 +00:00
Matthew Hodgson 6c7d13f8ce API to stop peeking 2016-03-18 19:22:34 +00:00
Richard van der Hoff e15a2d138c Merge pull request #112 from matrix-org/rav/cancel_send
Support for cancelling pending events
2016-03-18 16:17:44 +00:00
Richard van der Hoff 8bc9c19278 Merge pull request #111 from matrix-org/rav/pending_event_list
Implement 'pendingEventList'
2016-03-18 16:17:30 +00:00
David Baker dd86fade11 Merge pull request #113 from matrix-org/dbkr/threepid_lookup
Add a method to the js sdk to look up 3pids on the ID server.
2016-03-18 15:54:43 +00:00
David Baker ba1991aa8f Add more docs :) 2016-03-18 15:54:19 +00:00
David Baker f4fd8d9ba6 Add a method to the js sdk to look up 3pids on the ID server. 2016-03-18 15:15:10 +00:00
Richard van der Hoff 02be0f659a Support for cancelling pending events
Implement client.cancelPendingEvent which will cancel queued or not_sent events
2016-03-17 22:15:46 +00:00
Richard van der Hoff c7be310bdf Fix addPendingEvent invocation in unit test 2016-03-17 22:10:40 +00:00
Richard van der Hoff 55d8f56f98 update docs 2016-03-17 17:53:20 +00:00
Richard van der Hoff ab35fff9e8 Implement 'pendingEventList'
The existing 'pendingEventOrdering'=='end' semantics had been substantially
broken by the introduction of timelines and gappy syncs: after a gappy
sync, pending events would get stuck in the old timeline section. (Part of
https://github.com/vector-im/vector-web/issues/1120).
2016-03-17 17:05:23 +00:00
Richard van der Hoff fdbc7a3112 Merge pull request #110 from matrix-org/rav/refactor_remote_echo
Refactor transmitted-messages code
2016-03-17 16:40:47 +00:00
Richard van der Hoff 3c6bd4774d Refactor transmitted-messages code
This is some preparatory work for fixing up the problems with te timeline
ordering of unsent messages
(https://github.com/vector-im/vector-web/issues/1120). The functional changes
here should be minimal (bar an extra `Room.localEchoUpdated` when the local
echo is first added to the timeline).

Give `MatrixClient.sendEvent` its own entry point `Room.addPendingMessage`
instead of pushing it through `addEventsToTimeline`; this considerably
simplifies the implementation of the latter and also means that we can contain
the `_txnId` ming to MatrixClient.

Move the code which deals with a successful `/send` response from
`MatrixClient` into `Room.updatePendingEvent`, since it involves fiddling with
the innards of the Room.

Also adds a new EventStatus 'SENT' for events which have been successfully sent
but whose remote echo we still haven't received.
2016-03-17 14:26:36 +00:00
Richard van der Hoff a2861c5781 Merge pull request #109 from matrix-org/rav/log_sync_error_stack
Log the stack when we get a sync error
2016-03-17 14:24:07 +00:00
Richard van der Hoff eaf3fe16eb sync error: Don't log the exception twice
If we have e.stack, then it will include the description of the exception.
2016-03-17 12:05:01 +00:00
Richard van der Hoff 963eaf7ec7 Log the stack when we get a sync error
If we have the stack for an exception in the /sync loop, we should log it.
2016-03-17 11:54:43 +00:00
Richard van der Hoff e6e5b9b748 release.sh: fix -z option 2016-03-17 01:33:51 +00:00
Richard van der Hoff 9ad031c857 Make changelog file and jsdoc generation switchable 2016-03-17 01:27:48 +00:00
Richard van der Hoff a0d465cb34 Merge master to develop after release 2016-03-17 01:12:39 +00:00
Richard van der Hoff 2dcf5227f0 Merge remote-tracking branch 'origin/master' into develop 2016-03-17 01:12:18 +00:00
Richard van der Hoff 518e92027c Add missing "Changes in" to changelog 2016-03-17 01:07:34 +00:00
Matthew Hodgson ebc95667b8 workaround for unicode regexp matches - https://github.com/vector-im/vector-web/issues/568 2016-03-17 01:02:50 +00:00
Richard van der Hoff ad5d07caf8 0.4.2 2016-03-17 00:58:33 +00:00
Richard van der Hoff b2d7abc0a1 Prepare changelog for v0.4.2 2016-03-17 00:58:16 +00:00
Richard van der Hoff cc475e6392 add jsdoc as dev dependency 2016-03-17 00:58:16 +00:00
Richard van der Hoff e4c6717bd5 Don't fail release if dist dir already exists 2016-03-17 00:58:16 +00:00
Richard van der Hoff 53f813207e Add option to skip changelog generation 2016-03-17 00:45:39 +00:00
Richard van der Hoff 873fde27ac Don't error if changelog is unchanged 2016-03-17 00:33:01 +00:00
Richard van der Hoff 8d9d638953 release.sh: fix 'read' syntax 2016-03-17 00:22:37 +00:00
Richard van der Hoff 2f93490054 Don't create release branch if we're already there 2016-03-17 00:19:30 +00:00
Richard van der Hoff e22efc9dd5 release.sh: chmod +x 2016-03-16 23:40:44 +00:00
Richard van der Hoff e7ac80cf2b Script to do releases 2016-03-16 23:12:38 +00:00
Richard van der Hoff 4436087777 Use npm version to do release stuff 2016-03-16 23:12:38 +00:00
Matthew Hodgson f7bc11361c trivially add content.currently_active in m.presence events. 2016-03-16 22:35:55 +00:00
Matthew Hodgson a68b61dafe oops, revert accidental merge 2016-03-16 17:33:30 +00:00
Matthew Hodgson 84c9876b3a if synapse handed us profile data in the leave event, then use it. unbreaks overzealous tests 2016-03-16 17:32:47 +00:00
Matthew Hodgson de864c489a make sure we show display names & avatars on parts, and use the right type of content for displaynames for member events in general. fixes https://github.com/vector-im/vector-web/issues/1140 and https://github.com/vector-im/vector-web/issues/873 and a bunch more 2016-03-16 17:32:47 +00:00
Matthew Hodgson 2c277f7d96 Merge pull request #108 from matrix-org/matthew/fix-displaynames
Matthew/fix displaynames
2016-03-16 17:31:41 +00:00
Kegan Dougal d0560f594d Set the right .sender value for m.room.member events 2016-03-16 17:18:33 +00:00
Matthew Hodgson 60b6310494 typo 2016-03-16 16:47:25 +00:00
Matthew Hodgson abd27f9b75 failing test for https://github.com/vector-im/vector-web/issues/1140 2016-03-16 16:45:23 +00:00
Matthew Hodgson 3d316959f9 Revert this as it just doesn't work - our events are always m.room.members at this point 2016-03-16 16:44:22 +00:00
Matthew Hodgson 8aa3b79501 Merge pull request #107 from matrix-org/revert-106-matthew/fix-displaynames
Revert "make sure we show display names & avatars on parts, and use the right…"
2016-03-16 14:49:38 +00:00
Matthew Hodgson f35409700a Revert "make sure we show display names & avatars on parts, and use the right…" 2016-03-16 14:49:29 +00:00
Matthew Hodgson b009739b9e Merge pull request #106 from matrix-org/matthew/fix-displaynames
make sure we show display names & avatars on parts, and use the right…
2016-03-16 14:39:59 +00:00
Matthew Hodgson f007af741e Merge pull request #104 from matrix-org/matthew/may-client-send-state
Add RoomState.mayClientSendStateEvent()
2016-03-16 14:35:27 +00:00
Matthew Hodgson 3db4d9488b oops, normal events should use the chronologically earlier content, but membership changes should use the current content. 2016-03-16 14:31:03 +00:00
Matthew Hodgson 6b0fa84697 if synapse handed us profile data in the leave event, then use it. unbreaks overzealous tests 2016-03-16 14:14:14 +00:00
Matthew Hodgson 98b0cf2560 make sure we show display names & avatars on parts, and use the right type of content for displaynames for member events in general. fixes https://github.com/vector-im/vector-web/issues/1140 and https://github.com/vector-im/vector-web/issues/873 and a bunch more 2016-03-16 13:51:55 +00:00
Matthew Hodgson 372759b6e4 fix lint 2016-03-16 13:43:38 +00:00
Matthew Hodgson ec29b4ffeb Add RoomState.mayClientSendStateEvent() 2016-03-16 13:08:36 +00:00
Matthew Hodgson 95494933fd Merge pull request #103 from matrix-org/matthew/peek-presence
make presence work when peeking.
2016-03-16 11:55:25 +00:00
Matthew Hodgson 6fff29c07b oops, that map should be a forEach 2016-03-16 11:54:56 +00:00
David Baker 6f7ed93b87 Merge pull request #100 from matrix-org/dbkr/session_logged_out
Add Session.logged_out event
2016-03-16 10:44:01 +00:00
David Baker 8e903c0531 Merge pull request #94 from matrix-org/dbkr/may_send_state_event
Add maySendStateEvent method, ported from react-sdk (but fixed).
2016-03-16 10:39:55 +00:00
David Baker b90984a7f6 Use member.powerLevel instead of duplicating the user power level calculation. 2016-03-16 10:38:16 +00:00
David Baker 57006b7366 Check member hasn't left the room 2016-03-16 10:35:29 +00:00
Matthew Hodgson db9ba52873 make presence work when peeking. fixes https://github.com/vector-im/vector-web/issues/780 2016-03-15 21:50:18 +00:00
Richard van der Hoff 08b49c733a Merge pull request #101 from matrix-org/rav/remove_crypto_specialcase
Clean up a codepath that was only used for crypto messages
2016-03-15 17:26:54 +00:00
David Baker 0f38764709 No point throwing the exception if we return the original promise 2016-03-15 16:17:41 +00:00
Richard van der Hoff 6040b50ceb Fix another unit test
We ought to set the transaction_id in this test too
2016-03-15 15:49:21 +00:00
Richard van der Hoff b88a207bde Fix broken unit test
Fix broken unit tests which expected echoes to get matched up when
transaction_ids weren't set
2016-03-15 15:39:29 +00:00
Richard van der Hoff 07bbe358ea Clean up a codepath that was only used for crypto messages
Transmission of encrypted messages was happening somewhat differently to
normal messages. In particular, we weren't copying the 'unsigned' field when we
got the remote-echo, which meant the 'sync' code didn't correctly match up the
echo with the original.

The separate codepath was becoming a thorn in my side, so fix things up to
bring it back in line.
2016-03-15 15:07:26 +00:00
David Baker 39a5765888 Test tghat Session.logged_out is fired 2016-03-15 14:50:37 +00:00
David Baker 9f91995f4e Fix tests by returning the original promise to avoid the extra trip around the event loop. 2016-03-15 14:15:15 +00:00
David Baker 85f2754300 Make the client object be an event emitter rather than a matrixclient to avoid us being tempted to gut wrench stuff directly into the Matrix Client. 2016-03-15 11:05:05 +00:00
David Baker 5833654aa6 Add Session.logged_out event that fires whenever the current session is no longer valid and the user needs to log in again. Also null check _syncApi before trying to stop it. 2016-03-15 10:45:08 +00:00
David Baker 899ff6cea2 Merge pull request #99 from matrix-org/dbkr/sync_dont_tightloop
Add a delay before we start polling the connectivity check endpoint
2016-03-15 10:34:44 +00:00
David Baker 3752429b65 Fix the tests to tick the clock to 'wait' for sync retries. 2016-03-14 17:49:36 +00:00
David Baker d13fbd0e3e fix lint 2016-03-14 17:13:01 +00:00
David Baker 5e18c84e53 Add a delay before we start polling the connectivity check endpoint to avoid tightlooping if the conn check succeeds but /sync etc fails. 2016-03-14 16:50:00 +00:00
Richard van der Hoff fc1d5c86f9 Merge pull request #98 from matrix-org/rav/keep_paginating
Try again if a pagination request gives us no new messages
2016-03-14 15:48:17 +00:00
Richard van der Hoff c3ea913ae8 Try again if a pagination request gives us no new messages
This is basically a workaround for https://matrix.org/jira/browse/SYN-645: if
we knew about all of the events already, we want to try again.

Fixes the second half of https://github.com/vector-im/vector-web/issues/1014
2016-03-14 14:47:29 +00:00
Richard van der Hoff f2336aaedf Merge to master before develop
This avoids picking up things which have been landed on develop since the
release process started.
2016-03-11 12:17:29 +00:00
Richard van der Hoff 16ab2fd82a Add uglifyjs to devDependencies
The release script requires it, so we better install it
2016-03-11 12:15:00 +00:00
Richard van der Hoff 295fda027c Build 0.4.1 2016-03-11 12:00:44 +00:00
Richard van der Hoff 28e6f00766 Prep r0.4.1 2016-03-11 11:50:47 +00:00
Richard van der Hoff b1988de753 Merge branch 'master' into develop 2016-03-11 11:27:52 +00:00
Kegsay 0302eefd3c Update RELEASING.md 2016-03-11 11:05:55 +00:00
Kegsay 8ad5605e49 Update RELEASING.md 2016-03-11 11:03:00 +00:00
Kegsay 4b5539fe53 Create RELEASING.md 2016-03-11 10:58:39 +00:00
Richard van der Hoff b0becbc8d5 Merge pull request #97 from matrix-org/rav/updates_on_send_fail
Raise localEchoUpdated events in more places
2016-03-10 14:19:30 +00:00
Richard van der Hoff 4ae353d3d3 Raise localEchoUpdated events in more places
We need to know about more transitions for local-echo status changes, so raise
localEchoUpdated events for each transition.

Fixes an issue where we weren't turning failed transmissions red, because the
timeline wasn't being updated.
2016-03-10 12:07:27 +00:00
Richard van der Hoff b4b8b4bfb8 Merge pull request #95 from matrix-org/rav/raise_event_on_remote_echo
Emit an event when a local-echo is turned into a proper event
2016-03-08 16:40:03 +00:00
Richard van der Hoff 909a8a0648 Merge pull request #96 from matrix-org/rav/no_pagination_tightloop
Work around confused timelines causing pagination loops
2016-03-08 16:39:46 +00:00
Richard van der Hoff 234c227fd5 Work around confused timelines causing pagination loops
Look out for us getting stuck in a loop of using the same pagination token,
and use something else next time.

Hopefully this will fix https://github.com/vector-im/vector-web/issues/1089.
2016-03-08 15:38:00 +00:00
Richard van der Hoff 78eded3bbd Emit an event when a local-echo is turned into a proper event
We need to trigger an update of the timeline when this happens, so raise an
event for it.
2016-03-08 15:00:19 +00:00
David Baker f324e4c72f lint 2016-03-03 17:48:23 +00:00
David Baker 9328a12ccb Add maySendStateEvent method, ported from react-sdk (but fixed). Plus tests. 2016-03-03 17:44:27 +00:00
Richard van der Hoff 51380f8116 Merge pull request #93 from matrix-org/rav/another_timeline_race
Set the back-pagination token before raising Room.timelineReset
2016-03-02 11:41:20 +00:00
Richard van der Hoff cfd96969fc Add unit tests for setting the pagination token on sync 2016-03-01 13:58:29 +00:00
Richard van der Hoff 0034bdf4ad Set the back-pagination token before raising Room.timelineReset
This fixes another race condition on gappy syncs, wherein we weren't
back-paginating back from the start of the gappy sync.
2016-03-01 13:35:22 +00:00
Richard van der Hoff 00af1ce7d2 Merge pull request #92 from matrix-org/rav/sync_left_rooms_test
Add a unit test for syncLeftRooms
2016-03-01 13:19:12 +00:00
Richard van der Hoff 76f84c54db Add a unit test for syncLeftRooms
We don't have *any* tests for syncLeftRooms right now, so start one.
2016-03-01 12:12:49 +00:00
Mark Haines bb4766c8c6 Merge pull request #90 from matrix-org/markjh/change_push_actions
Add setPushRuleActions method for setting the actions for push notifi…
2016-03-01 10:10:38 +00:00
Richard van der Hoff e287e7591b Merge pull request #91 from matrix-org/rav/fix_stuck_pagination_after_join
Don't reset the timeline when we join a room after peeking
2016-03-01 09:10:21 +00:00
David Baker 48f7aca121 Merge pull request #89 from matrix-org/dbkr/invite_name_from_member_event
Use our inviter's member event to get their display name if it exists.
2016-02-29 18:05:27 +00:00
Richard van der Hoff a14f9e6d1c Don't reset the timeline when we join a room after peeking
If we've already got all the events in a limited sync, there is no need to reset
the timeline.
2016-02-29 17:25:20 +00:00
David Baker 5fefcd8ce3 pep8 2016-02-29 13:53:55 +00:00
David Baker 76f1d24c7b Make room name generation slightly more sane and add unit tests fir invite naming. 2016-02-29 13:51:55 +00:00
Mark Haines 066dd77aba Add setPushRuleActions method for setting the actions for push notification rules 2016-02-26 16:47:22 +00:00
David Baker 45a3bf63b2 Use our inviter's member event to get their display name if it exists. 2016-02-26 14:11:10 +00:00
Richard van der Hoff 75f2efffac Merge pull request #88 from matrix-org/rav/optimise_timeline_load
TimelineWindow.load: make the livetimeline case quicker
2016-02-26 13:27:40 +00:00
Richard van der Hoff 0584ec3319 Merge pull request #87 from matrix-org/rav/reset_timeline
Fire a 'Room.timelineReset' event when we get a gappy sync
2016-02-26 13:27:25 +00:00
Richard van der Hoff 38e81ba61a TimelineWindow.load: make the livetimeline case quicker
Avoid doing a loop round the reactor if we are just loading the live timeline.
2016-02-26 12:45:28 +00:00
Richard van der Hoff 164e4814af Merge remote-tracking branch 'origin/develop' into rav/reset_timeline 2016-02-25 18:35:53 +00:00
Richard van der Hoff abf908b14f Fire a 'Room.timelineReset' event when we get a gappy sync
We need to reset things at the UI level when we get a gappy sync, so give the
clients something to listen for.

Also add a bunch of tests for that bit of code.
2016-02-25 18:26:11 +00:00
Richard van der Hoff 1deb2e27d8 .npmignore which includes git-revision.txt 2016-02-25 17:50:54 +00:00
Richard van der Hoff 848ffe8a40 Merge branch 'master' into develop 2016-02-25 17:29:39 +00:00
Richard van der Hoff 79c10c1b68 Remove old tarball before building new one 2016-02-25 17:23:54 +00:00
Richard van der Hoff 7e6eb89524 Empty commit to kick jenkins 2016-02-25 17:22:53 +00:00
Richard van der Hoff 3d3e52b104 s/version.txt/git-revision.txt/ 2016-02-25 16:48:41 +00:00
Richard van der Hoff 05326984df Add a 'version.txt' file to the tarball
This will enable the vector build to know what it got
2016-02-25 15:21:23 +00:00
Richard van der Hoff e97e3c673f jenkins.sh: Run npm pack after build to build tarball 2016-02-25 13:20:13 +00:00
Richard van der Hoff f0ae46afc9 add jenkins.sh script 2016-02-25 13:17:38 +00:00
Richard van der Hoff aaf4371fae Merge pull request #85 from matrix-org/rav/recreate_filter_on_changes
Check filters before we reuse them
2016-02-24 17:22:57 +00:00
David Baker 7728009ef3 Changelog 2016-02-24 16:20:46 +00:00
Richard van der Hoff 46912431cc make the tests pass again 2016-02-24 16:15:08 +00:00
Richard van der Hoff 6a19e08381 lint 2016-02-24 15:58:35 +00:00
Richard van der Hoff 43f392955d Check filters before we reuse them
Make sure that we check the content of existing filters before we blindly reuse
them.

Fixes https://github.com/vector-im/vector-web/issues/988
2016-02-24 15:23:42 +00:00
David Baker 41d2076bd4 Merge branch 'develop' 2016-02-24 13:52:10 +00:00
David Baker 670d230f2e Bump to 0.4.0 2016-02-24 13:49:58 +00:00
David Baker 7970f3f5a5 Merge pull request #84 from matrix-org/dbkr/keypair_3pid_invites
Add support for new keypair style 3pid invites
2016-02-23 16:37:48 +00:00
David Baker 567716c4f7 Use more normal promise structure 2016-02-23 11:21:29 +00:00
David Baker 518e41c078 add docs 2016-02-23 11:08:07 +00:00
David Baker bd600f65fb Add support for new keypair style 3pid invites (add an option to joinRoom for specifying the signing url) 2016-02-23 10:11:04 +00:00
Matthew Hodgson 363b08c4d8 don't NPE on 50x's - as per BOTS-170 2016-02-22 10:34:43 +00:00
Matthew Hodgson 2150bdc444 fix tests 2016-02-19 17:59:26 +00:00
Matthew Hodgson 5886b3358d f1x l1nt 2016-02-19 17:56:55 +00:00
Matthew Hodgson 8b887d8559 name 3PID invite rooms better 2016-02-19 17:45:57 +00:00
Richard van der Hoff 7278c38fa6 Merge pull request #83 from matrix-org/rav/fix_context_event_ordering
Interpret the response from /context correctly
2016-02-19 17:12:48 +00:00
Richard van der Hoff 24ae4a8d1a Interpret the response from /context correctly
events_before are backwards

Fixes https://github.com/vector-im/vector-web/issues/963
2016-02-19 17:03:47 +00:00
David Baker 923b9cad39 Merge pull request #82 from matrix-org/real_receipts
Add a param to getEventReadUpTo to have it ignore implicit read receipts
2016-02-19 16:20:04 +00:00
David Baker e9f6e41550 Local echos are fake too. 2016-02-19 16:18:29 +00:00
David Baker 2950417f70 Add docs to appease jslint 2016-02-19 15:35:36 +00:00
David Baker 39f641a851 Address PR comments 2016-02-19 15:22:38 +00:00
David Baker 95fff38dbb Add a param to getEventReadUpTo to have it ignore implicit read receipts. Store real receipts separately to make this work. 2016-02-19 14:42:07 +00:00
Richard van der Hoff 785326376a Merge pull request #80 from matrix-org/rav/keep_redactions
Keep redacted events in the timeline
2016-02-17 21:41:42 +00:00
Richard van der Hoff 1baf14861c Merge pull request #81 from matrix-org/rav/fix_timeline_after_join
EventTimeline: Fix baseIndex after removing the last event
2016-02-17 12:23:29 +00:00
Richard van der Hoff 8e47fe2968 Fix lint 2016-02-16 22:32:50 +00:00
Richard van der Hoff 88827fab84 EventTimeline: Fix baseIndex after removing the last event
Removing the last event in an EventTimeline (as we might, for instance, if it
was a local echo in an empty timeline) got us into a state where the baseIndex
would increment when adding events to the end of the timeline, causing much
confusion.
2016-02-16 22:22:26 +00:00
Richard van der Hoff ab0a06eea7 More delinting 2016-02-16 16:19:32 +00:00
Richard van der Hoff 6a6db36088 delintificate 2016-02-16 16:14:45 +00:00
Richard van der Hoff 6f3bdcfbb6 Remember to propagate Room.redaction 2016-02-16 16:03:40 +00:00
Richard van der Hoff 4c6d0a5128 Redactions: only remove the keys that are specced for removal 2016-02-16 16:03:18 +00:00
Richard van der Hoff 5eff278454 Keep redacted events in the timeline
Everything gets confused when we remove events from timelines, so keep
redacted events in there, and mark them as redacted instead.
2016-02-16 12:05:13 +00:00
David Baker c8d1e210a3 Merge pull request #79 from matrix-org/dbkr/new_reconnection_logic
Use only /versions requests for connection recovery
2016-02-10 11:17:29 +00:00
David Baker 8614632e54 No ES6 in the JS SDK and other lint warnings. 2016-02-10 10:48:03 +00:00
David Baker c3796c61cd Use a promise for when the connection comes back 2016-02-10 10:40:42 +00:00
David Baker 6224aff882 improve comment as per PR review 2016-02-10 10:09:39 +00:00
David Baker e506a1b2de Oops: set keepAliveTimer! 2016-02-09 17:39:30 +00:00
David Baker b40a6d1481 remember bound function as it returns a new reference each time 2016-02-09 17:06:20 +00:00
David Baker 5c8f73019e lint 2016-02-09 16:22:12 +00:00
David Baker 3cfc4f8ba5 Add short delay to treating a 400 as a success to avoid hammering in a loop. 2016-02-09 16:17:52 +00:00
David Baker 4d46251b15 Just use the keepalive logic to recover from lost internet connections.
* This should fix the problem where we could end up with two concurrent syncs
   due to both retrying /sync and hitting /versions. There is now one and only
   one mechanism for connection recovery.
 * Hit /versions a little less often as I think every 2 seconds is a little
   over-aggressive. Also introduce randomness to minimize possibility of
   thundering herds.
 * Add a listener for the 'online' event in case any browser is nice enough
   to ever fire it.
 * Treat a 400 response from /versions as successful since older synapses
   will not support it.
2016-02-09 16:08:14 +00:00
David Baker 977e33f1bd Merge pull request #76 from matrix-org/dbkr/sync_gutwrench_less
Change the sync state tracking slightly to gut-wrench client a bit less
2016-02-08 14:18:59 +00:00
David Baker daa0e6291e Add docs for STOPPED state and correct js doc 2016-02-08 14:18:15 +00:00
Daniel Wagner-Hall 620417ed1b Merge pull request #78 from matrix-org/daniel/r0
Use /r0 or /unstable for all requests
2016-02-08 11:57:13 +00:00
Daniel Wagner-Hall 02196416e4 Use /r0 or /unstable for all requests 2016-02-08 11:15:30 +00:00
Richard van der Hoff 5ca4bc6b85 Merge pull request #77 from matrix-org/rav/check_old_timelines_for_echoes
Check old timelines when looking for echoes of sent messages
2016-02-05 19:41:37 +00:00
Richard van der Hoff 76c79ec299 Check old timelines when looking for echoes of sent messages
After /send completes, we check the room to see if the echo has already come
back via /sync. It's a bit of an edge-case, but we ought to check all
timelines, not just the live one.

Furthermore, we now have a map from eventId to timeline, so we can handle the
case where the echo has *not* yet come back more efficiently than searching
through the whole timeline.
2016-02-05 17:31:46 +00:00
David Baker fc730d4637 remove unintentional log line 2016-02-05 14:13:14 +00:00
David Baker 41d5917bb6 Change the sync state tracking slightly to gut-wrench client a bit less
Should be functionally identical outside of client and sync.
2016-02-05 14:09:58 +00:00
Richard van der Hoff 122867e8ee Merge pull request #75 from matrix-org/rav/read_receipt_ordering
Give precedence to later Read Receipts
2016-02-04 16:50:59 +01:00
Richard van der Hoff f3e5e03009 Give precedence to later Read Receipts
In order to resolve the conflict between local and remote read-receipts, try to
give precedence to the read-receipt which refers to the most recent event.

Also fix the read-receipt synthesis in _addLiveEvents so that it actually works
(drop the spurious MatrixEvent wrapper), and remove the synthesis in
recalculate() (which appears to be redundant).
2016-02-04 15:35:27 +00:00
Richard van der Hoff 1b43e5ed98 Merge pull request #74 from matrix-org/rav/fix_localecho_timeline_bug
Fix a bug which made the timelines get confused about local messages
2016-02-03 15:41:52 +01:00
Richard van der Hoff 9e65f12ddd Fix lint warnings 2016-02-03 14:29:39 +00:00
Richard van der Hoff be297ddbcd Merge pull request #73 from matrix-org/rav/fix_timeline_tightloop
Make sure we don't end up calling /messages in a loop if things go weird
2016-02-03 15:14:41 +01:00
Richard van der Hoff 8ee1d17ff7 Fix a bug which made the timelines get confused about local messages
Make sure that the timeline index is kept consistent when the id of an event
changes when we receive the remote echo of a message we sent.
2016-02-03 11:56:09 +00:00
Richard van der Hoff a2185fefc1 Make sure we don't end up calling /messages in a loop if things go weird
If we somehow end up in a situation where calling /messages returns a load
of messages, but none of them are new, then currently we start calling
/messages again and again in a tight loop. This is bad, so fix it.
2016-02-03 11:47:04 +00:00
David Baker f172272dd3 Merge pull request #72 from matrix-org/dbkr/document_getroom
Add docs to getRoom noting how you shouldn't assume it will return so…
2016-02-02 17:16:30 +00:00
David Baker 8a77b29d17 Add docs to getRoom noting how you shouldn't assume it will return something because you've got a Room-like event 2016-02-02 16:30:28 +00:00
Richard van der Hoff 0a7efe3e8b Fix undefined variable accesses
fix a merge error in the last commit
2016-01-30 00:44:40 +00:00
Richard van der Hoff 2af947fc1a Stash the old next_batch token for pagination
When we reset the live timeline due to a limited sync, stash next_batch as the
pagination token so that we can work forward to the new live timeline.

(Note that this requires matrix-org/synapse#535)
2016-01-30 00:12:31 +00:00
Richard van der Hoff 1499087098 TimelineWindow: fix canPaginate during load
We should return false rather than throw an exception if someone calls
canPaginate before the timeline finishes loading.
2016-01-30 00:09:12 +00:00
David Baker 672e96b90e Merge pull request #71 from matrix-org/dbkr/separate_sync_processing
Separate the actual processing of the sync response from the loop doing the requests.
2016-01-29 13:05:27 +00:00
David Baker 3d5e2937e2 docstring 2016-01-29 11:29:14 +00:00
David Baker bbf3b2637a Re-apply: 5297855ad3
Separate the actual processing of the sync response from the loop doing the requests.
2016-01-29 11:25:01 +00:00
David Baker 04a1c4f1a2 Revert "Separate the actual processing of the sync response from the loop doing the requests."
This reverts commit 5297855ad3.

Accidentally committed to wrong branch
2016-01-29 11:17:12 +00:00
David Baker 5297855ad3 Separate the actual processing of the sync response from the loop doing the requests. 2016-01-29 11:05:32 +00:00
Richard van der Hoff a221674680 Merge pull request #64 from matrix-org/rav/timeline_window
TimelineWindow object
2016-01-28 16:46:03 +00:00
Richard van der Hoff 8db95f42fb Add some unit tests for TimelineWindow. 2016-01-28 16:38:45 +00:00
Richard van der Hoff b42e5d5fcf Address review comments
Rearrange a couple of things for clarity, and add some comments.
2016-01-28 12:36:12 +00:00
Richard van der Hoff b1e2090eef TimelineWindow object
A handy thing for tracking a window into a room timeline

Could really do with some unit tests... sorry.
2016-01-27 09:53:15 +00:00
Richard van der Hoff 8716185f4a Merge pull request #63 from matrix-org/rav/context
Support for non-contiguous event timelines
2016-01-27 09:50:57 +00:00
Richard van der Hoff 3c2fad7c8d Merge remote-tracking branch 'origin/develop' into rav/context 2016-01-27 09:49:00 +00:00
Richard van der Hoff 60a243f160 Rename 'exceptFail' to 'failTest', and move it out to test-utils.js 2016-01-27 09:48:28 +00:00
Richard van der Hoff a87cefa035 Replace the boolean args on EventTimeline methods with constants 2016-01-26 22:38:26 +00:00
Richard van der Hoff 101d3952d3 Test that the pagination tokens actually start at null 2016-01-26 21:25:10 +00:00
Richard van der Hoff a01501b42c Address a number of review comments.
Make sure that room state is copied correctly when resetting the live
timeline.

Also comments and bits.
2016-01-26 18:09:15 +00:00
David Baker d37369b463 Merge pull request #70 from matrix-org/dbkr/receipt_local_echo
Add local echo for read receipts.
2016-01-26 14:11:49 +00:00
David Baker 0c3abcccf2 camelCase 2016-01-26 14:11:36 +00:00
Richard van der Hoff 48d1bc3158 Fix incompatibility with peeking.
The peek code needs to make sure it sets the pagination token /after/ adding
the events to the timeline, otherwise it will get reset when the events
are added.
2016-01-26 11:21:49 +00:00
David Baker 15e8784daf Add local echo for read receipts. Fixes https://github.com/vector-im/vector-web/issues/623 2016-01-25 17:49:41 +00:00
Richard van der Hoff 840b8f0bc0 Merge branch 'develop' into rav/context
Conflicts:
	lib/models/room.js
2016-01-25 10:45:22 +00:00
Kegsay 7a4cc62280 Merge pull request #69 from matrix-org/kegan/sync-keep-alive
Implement a keep-alive timer for /sync requests
2016-01-22 16:23:43 +00:00
Kegan Dougal f8ec35691f Interrupt /sync backoff when keep-alive succeeds in order to immediately retry if we were waiting 2016-01-21 18:02:26 +00:00
Kegan Dougal c5e7df8975 Hit /versions instead of / since it is actually a known endpoint 2016-01-21 17:52:52 +00:00
Kegan Dougal 7bdab05785 Unbreak tests 2016-01-21 17:34:12 +00:00
Kegan Dougal 197144dcda Implement a keep-alive timer for /sync requests
When a /sync request fails, we spin up a keep-alive poll to /_matrix/client/r0
which 400s. We treat any HTTP response code as a success for the purposes of
polling the server. When a successful poll is done, we shoot the current /sync
request in the head immediately (via a hacky abort() on the promise) and retry
the /sync.
2016-01-21 17:17:27 +00:00
Kegan Dougal ff990914b2 Remove low client-side timeouts hack 2016-01-21 16:12:07 +00:00
David Baker 4bc2869522 Merge pull request #68 from matrix-org/dbkr/new_unread_count_format
Update for new unread count format
2016-01-21 09:54:51 +00:00
David Baker 1f1d743678 Merge remote-tracking branch 'origin/develop' into dbkr/new_unread_count_format 2016-01-21 09:50:20 +00:00
Matthew Hodgson 787c0ebabc fix another test 2016-01-21 00:14:29 +00:00
Matthew Hodgson d559ad794a STUPID LINE LENGTH LIMITS 2016-01-21 00:10:15 +00:00
Matthew Hodgson 24655ac60e missing semicolon 2016-01-21 00:02:24 +00:00
Matthew Hodgson 5fd0ea2f6f fix test 2016-01-20 23:56:16 +00:00
Matthew Hodgson eaf7b03bb1 if we are the only person in a room, call it an 'Empty room' too, given this is how humans see a room if they're the only person in it... 2016-01-20 23:55:09 +00:00
David Baker d375804cd6 Merge branch 'develop' into dbkr/new_unread_count_format 2016-01-20 18:53:14 +00:00
David Baker a24a9d35c4 Fix PR comments: typos and redundant line 2016-01-20 18:52:32 +00:00
Kegan Dougal a0d81fccdb Fix test 2016-01-20 17:29:27 +00:00
David Baker b4e4aaff00 Merge branch 'develop' into dbkr/new_unread_count_format 2016-01-20 17:25:54 +00:00
Matthew Hodgson 3a73b54e4a .name defaults to mxid 2016-01-20 17:22:16 +00:00
David Baker 5ec0fce2a4 style 2016-01-20 17:19:26 +00:00
Matthew Hodgson 8b7497374f name self-chats by displayname if possible rather than mxid, and name empty-chats as 'Empty room' rather than the fugly '?' 2016-01-20 17:19:13 +00:00
David Baker 8cb180525e Add getter/setter for unread notif counts. 2016-01-20 17:16:20 +00:00
Kegan Dougal 6df9d08dc1 Fix tests 2016-01-20 15:59:34 +00:00
David Baker b3c06dd723 Update for new unread count format 2016-01-20 15:59:06 +00:00
Kegan Dougal 2a88b8db4e Improve performance of hasMembershipState to not be stupid 2016-01-20 15:09:35 +00:00
Kegsay 31c29b7e5e Merge pull request #67 from matrix-org/kegan/address-book
Make getUsers() return users for *EEEEEVERYOOOOONE*
2016-01-19 12:51:16 +00:00
Kegan Dougal 865db906e3 Make getUsers() return users for *EEEEEVERYOOOOONE* regardless of presence events 2016-01-19 11:40:08 +00:00
manuroe 0b52a7e7c9 Merge branch 'push-rules-settings' into develop
# Conflicts:
#	lib/sync.js
2016-01-18 17:35:51 +01:00
manuroe 80f7220a7b Merge branch 'develop' into push-rules-settings
# Conflicts:
#	lib/sync.js
2016-01-18 17:33:13 +01:00
manuroe 2e664adb32 Updated push rules methods after review 2016-01-18 17:22:35 +01:00
Kegan Dougal 14ef9348be Add getUsers() 2016-01-18 12:05:26 +00:00
Kegsay 3c170bf063 Merge pull request #66 from matrix-org/matthew/roomsettings2
deleteAlias() support
2016-01-18 10:27:58 +00:00
Kegan Dougal 3e67406a30 Tweak docs 2016-01-18 10:27:43 +00:00
Matthew Hodgson d6075bb5bd add an XXX 2016-01-17 23:32:00 +00:00
Matthew Hodgson d8e56dad1b oops 2016-01-16 01:01:11 +00:00
Matthew Hodgson 67872206ff deleteAlias() support 2016-01-16 00:58:44 +00:00
Richard van der Hoff 43e7173c30 fix some racy tests 2016-01-16 00:18:51 +00:00
Richard van der Hoff e0ddd65922 Address review comments
Improve comments and naming.
2016-01-15 23:50:09 +00:00
Richard van der Hoff dfb2fa821d minor jsdoc fixes 2016-01-15 17:11:00 +00:00
Richard van der Hoff fce7248ed5 Flag the top of the timeline when we hit it 2016-01-15 17:10:01 +00:00
Richard van der Hoff e68ab7d54a Tweak duplicateStrategy code to reduce diff 2016-01-15 13:23:51 +00:00
Richard van der Hoff 706966ffe9 Support for non-contiguous event timelines
This provides optional support for fetching old events via the /context API,
and paginating backwards and forwards from them, eventually merging into the
live timeline.

To support it, events are now stored in an EventTimeline, rather than directly
in an array in the Room; the old names are maintained as references for
compatibility.

The feature has to be enabled explicitly, otherwise it would be impossible for
existing clients to back-paginate to the old events after a gappy /sync.

Still TODO here:

* An object which provides a window into the timelines to make them possible to
  use. This will be a separate PR.

* Rewrite the 'EventContext' used by the searchRoomEvents API in terms of an
  EventTimeline - it is essentially a subset.
2016-01-15 13:19:11 +00:00
manuroe e3b4cb03e1 Fixed Jenkins tests complaints 2016-01-14 19:17:08 +01:00
manuroe 8011aab561 Added MatrixClient getRoomPushRule and setRoomMutePushRule methods 2016-01-14 18:58:15 +01:00
Matthew Hodgson a8d24798e6 oooooooops... let's pretend nobody saw this 2016-01-14 17:23:04 +00:00
Kegan Dougal e4c38ac78c Linting 2016-01-13 15:31:27 +00:00
Kegan Dougal 5fa6f0037f Cache the third_party_invite token to allow constant time lookups 2016-01-13 15:31:27 +00:00
Matthew Hodgson a0df2a70cd s/getImplicitRoomName/getDefaultRoomName/ # as kegan doesn't like the word 'implicit' 2016-01-13 14:02:26 +00:00
Kegan Dougal 0bab00c47c Add debug logging to sync polling. Add speculative fix for vector-im/vector-web#544 2016-01-13 13:17:21 +00:00
Matthew Hodgson f48fb34818 Merge pull request #62 from matrix-org/matthew/roomsettings2
Room.getImplicitRoomName
2016-01-13 12:58:52 +00:00
Matthew Hodgson 8810ff2256 merge and add null check 2016-01-13 12:58:46 +00:00
Matthew Hodgson 17efc5163f Merge branch 'develop' into matthew/roomsettings2 2016-01-13 12:55:30 +00:00
Matthew Hodgson 3ce07a020d ooops - fix getDomain expression 2016-01-13 12:52:22 +00:00
Matthew Hodgson 71abef0117 fix merge conflict 2016-01-13 12:46:47 +00:00
Matthew Hodgson a79270b8f8 Merge pull request #61 from matrix-org/matthew/accountdata
implement account data
2016-01-13 12:43:53 +00:00
Matthew Hodgson 87db054e22 fix jsdoc 2016-01-13 12:43:42 +00:00
Kegan Dougal ae06dd2ab8 Fix thinko where idServerRequests return strings 2016-01-12 17:23:50 +00:00
Matthew Hodgson 88c7293838 based on PR review, rewrite account_data support to avoid tracking the section that events came from, and instead having /sync results piped into the right bit of the room directly 2016-01-11 19:25:44 +00:00
Matthew Hodgson 051e83582b add a cli.getDomain() method rather than react-sdk maintaining its own (multiple) implementations 2016-01-11 18:23:07 +00:00
Matthew Hodgson 57072bc4f4 s/implicit/ignoreRoomNameEvent/ on calculateRoomName 2016-01-11 18:20:26 +00:00
Kegan Dougal e8f77256de Set the updated .sender and .target props on the event when the event itself updates these props. 2016-01-11 17:35:46 +00:00
Kegan Dougal 51fe73bc27 Return v2 prev_content when calling getPrevContent() 2016-01-11 17:15:52 +00:00
Kegan Dougal 97003f7382 Use timeout=0 rather than timeout=1 because of SYN-588
Fixes https://github.com/vector-im/vector-web/issues/606
2016-01-11 16:33:43 +00:00
manuroe c10218a1fa Fixed Jenkins tests complaints 2016-01-11 17:12:28 +01:00
manuroe 5c59a2ea3e Fixed MatrixClient pushRules method and ivar clash.
Added MatrixClient.setPushRuleEnabled method.
2016-01-11 17:00:05 +01:00
Kegsay 0b79ac1386 Merge pull request #60 from matrix-org/kegan/guest-access
Add constructs for guest access
2016-01-11 15:19:14 +00:00
Kegan Dougal 4fe95f18b9 More commenting on the creation of guest filters 2016-01-11 14:52:06 +00:00
Kegan Dougal db5ca49ee2 Linting 2016-01-11 09:31:00 +00:00
Matthew Hodgson d7158b575f fix trailing space 2016-01-10 20:05:58 +00:00
Matthew Hodgson 678d70528e add a Room.getImplicitRoomName so clients can know what a room would be called if it didn't have an explicit m.room.name state event 2016-01-10 20:02:35 +00:00
David Baker 02b33766ee Document the order of the room timeline because I can never remember which way round it is. 2016-01-08 20:26:07 +00:00
Matthew Hodgson 9bd45cf7c7 more lint and thinkos 2016-01-08 03:45:05 +00:00
Matthew Hodgson c64aebdb17 lint and thinkos 2016-01-08 03:41:05 +00:00
Matthew Hodgson 387ad09c5f implement account data 2016-01-08 03:22:08 +00:00
Kegan Dougal ea3bd1450e Add functions to support upgrading guest accounts 2016-01-07 17:23:56 +00:00
Kegan Dougal 8f4bd9c693 NOP typing/receipts when a guest 2016-01-07 15:03:37 +00:00
Kegan Dougal cdb4bc5107 Implement peek syncing.
This involves hitting room initial sync then /events?room_id=!thing:here
It even works.
2016-01-07 14:58:28 +00:00
Matthew Hodgson 446faed9b5 copyrights please... 2016-01-07 04:15:38 +00:00
Kegan Dougal d36c928d95 Fix tests 2016-01-06 17:35:56 +00:00
Kegan Dougal 3a3f25c1bc Remove guest rooms array; replace with a peeking SyncApi
After much discussion, the HS will now behave the same for guests/non-guests
wrt joining a room (you get the entire room state on join). This leave "peeking"
which never triggers a join. This can be implemented for guests by doing a
room initial sync followed by a specific /events poll with a specific room_id.
This means there are 2 sync streams: /sync and the peek /events. Architected
so you can only have 1 peek stream in progress at a time (if this were arbitrary
we'd quickly run into concurrent in-flight browser request limits (5).
2016-01-06 17:29:14 +00:00
Kegan Dougal 73e65bc18b Add setGuestAccess to allow easy room guest access configuration. 2016-01-05 17:09:10 +00:00
Kegan Dougal 445491c4ad Fix guest rooms UT to reflect reality 2016-01-05 16:57:59 +00:00
Kegan Dougal f12499c6bf Support guest room filters 2016-01-05 13:24:51 +00:00
Kegan Dougal 8c6c65ab6c Don't do requests we know are going to fail as a guest 2016-01-05 11:50:24 +00:00
Richard van der Hoff b85e267fdb Merge pull request #59 from matrix-org/rav/event_context
Enhancements to search results, and event context implementation

This change adds support to the JDK for processing the results of a room
search, as well as back-paginating the results.

It treats each search result as a 'context' object, which can itself be
backwards or forward-paginated.
2016-01-04 12:50:10 +00:00
Richard van der Hoff c669d21af7 Enhancements to search results, and event context implementation
This change adds support to the JDK for processing the results of a room
search, as well as back-paginating the results.

It treats each search result as a 'context' object, which can itself be
backwards or forward-paginated.
2016-01-04 12:50:07 +00:00
Kegsay 3e4cef89fd Merge pull request #58 from matrix-org/notif_sync
Propagate the unread count from sync onto rooms
2015-12-23 10:07:13 +00:00
David Baker a06d1f62d7 Merge remote-tracking branch 'origin/develop' into notif_sync 2015-12-22 14:47:39 +00:00
Kegan Dougal 2802092231 Add 60s to client-side timeout to account for slow HSes 2015-12-21 13:49:14 +00:00
Richard van der Hoff a419e241a6 Merge pull request #57 from matrix-org/rav/search_backfill
Allow passing a next_batch token into /search for backfill
2015-12-21 09:15:53 +00:00
David Baker 7aa4bd7f46 Propagate unread notif count from sync to the room object 2015-12-18 17:49:42 +00:00
Kegsay 2eec76bc1d Merge pull request #56 from matrix-org/kegan/archived-rooms
Add syncLeftRooms()
2015-12-18 16:57:17 +00:00
Richard van der Hoff 13fcff9688 Allow passing a next_batch token into /search for backfill 2015-12-18 16:23:48 +00:00
Kegan Dougal 1174147d64 Don't lie in comments; remove spurious flag setting 2015-12-18 15:29:28 +00:00
Kegan Dougal de53b292a2 Remove debug logging 2015-12-18 15:21:28 +00:00
Kegan Dougal b50d61428c Impl syncing of left rooms. Factor out getting or creating filters. 2015-12-18 11:57:46 +00:00
Kegan Dougal 4bc5343b67 Merge branch 'develop' into kegan/archived-rooms 2015-12-18 09:47:43 +00:00
Kegan Dougal da560ffeff Fix SYJS-38 - Events stuck in 'sending' state
v1 used to clobber events after sending so `.status` would be `null`. v2 is
smarter and just clobbers the `.event` data so references to the local echo
event would reflect the new event. However, the `.status` in this case would
still have the old value (SENDING), so make sure to reset it after the 200 OK
from sending the event.
2015-12-18 09:14:48 +00:00
Kegan Dougal 59965b1c59 Add public api for syncing left rooms. Not implemented yet. 2015-12-17 17:17:53 +00:00
Kegsay 431f4a4797 Merge pull request #55 from matrix-org/kegan/sync-exp-backoff
Reset /sync backoff if setTimeout takes a long time to fire
2015-12-17 16:42:22 +00:00
Kegan Dougal 40c3fe558c Add note stating when timing delays can occur. 2015-12-17 16:31:00 +00:00
Kegan Dougal a12cd8d4a0 Fix JSDoc 2015-12-17 16:18:28 +00:00
Kegan Dougal ac7a469582 Add setIncludeLeaveRooms 2015-12-17 15:53:56 +00:00
Kegan Dougal 2e9376614f BREAKING: Add bindEmail option to register() 2015-12-17 14:13:17 +00:00
Kegan Dougal cd9d1daf17 Reset /sync backoff if setTimeout takes a long time to fire
This fixes vector-web/vector-im#536

The bug here is that we were assuming `setTimeout` would fire after the
requested time. This is not true when the machine is asleep. We now timestamp
before and after `setTimeout` and reset the attempt count if we have waited
more than twice what we originally requested. This allows for some jitter which
is to be expected.
2015-12-17 11:27:21 +00:00
Kegan Dougal bfa8dd0007 Cache the local event ID else we can remove a real event. Fixes vector-im/vector-web#530 2015-12-16 17:51:12 +00:00
Kegan Dougal 7e35ef258f Delete the room when it is forgotten and fire a new experimental event 2015-12-16 16:25:09 +00:00
Kegan Dougal 8bd43a8d53 Add /forget 2015-12-16 15:57:29 +00:00
Kegsay 8b87c0045d Merge pull request #54 from matrix-org/kegan/stale-call-notifications
Do not erroneously emit Call.incoming events on startup
2015-12-15 17:03:03 +00:00
Kegan Dougal 77356f0007 Fix vector-im/vector-web#494 2015-12-15 16:54:26 +00:00
Kegan Dougal 4cd6f615b3 Do bear minimum leave room handling so rejecting invites / leaving rooms are displayed correctly. 2015-12-15 16:21:36 +00:00
Kegan Dougal 65ef1dfd75 Lint and tests 2015-12-15 15:57:24 +00:00
Kegan Dougal 5719d513b7 Strip protocol part when doing 3PID invites for. Partially fixes vector-im/vector-web#419 2015-12-15 15:52:52 +00:00
Kegsay b90697264c Follow the spec 2015-12-15 15:33:56 +00:00
Kegan Dougal 406a2bb001 Do not erroneously emit Call.incoming events on startup
Check to see if the call was answered or hung up in addition to having a valid
lifetime before emitting the event. Fixes vector-im/vector-web#344
2015-12-15 15:13:41 +00:00
Kegsay 3115043b94 Merge pull request #53 from matrix-org/kegan/v2-sync
Use /sync instead of /initialSync and /events
2015-12-15 14:24:26 +00:00
Kegan Dougal bfda04daea Move local timeout logic to the HTTP API class. Fixes /sync bug
The ability to set a local timeout is applicable to any request, so move it
to http-api.js - We only use this on /sync requests currently. This simplifies
the SyncApi since it doesn't need to worry about it anymore -- the request
promise just gets rejected if the timer expires.

Whilst testing I noticed a weird anomaly which I cannot explain. Playing with
Chrome's network debugger, once you recover from a black hole (0kbps, 90s RTT)
the subsequent requests take 20s to return *even though there is no throttling*.
This was causing issues when using a local timer of timeout= and BUFFER_PERIOD_MS
when timeout=1 due to attempts>1 - they were being knifed before the response
could return. The 20s latency was entirely artifical (checked synapse logs and
they were sub 1s), but I cannot find anywhere in the JS-SDK or browser-requests
where this would be the cause. This persisted even when BUFFER_PERIOD_MS was
changed from 20s to 10s.
2015-12-15 11:59:41 +00:00
Kegan Dougal 46504b8b9f Fix tests; need more paranoia 2015-12-14 14:16:47 +00:00
Kegan Dougal f48c9175e5 Linting 2015-12-14 14:12:49 +00:00
Kegan Dougal bd4d8433ab Fix catchup bugs caused by using a stale pagination token 2015-12-14 14:11:25 +00:00
Kegan Dougal a00e318d73 Fix pagination - set prev_batch at the right time 2015-12-14 12:28:04 +00:00
Kegan Dougal fcf1abb185 Use v2 transaction IDs to suppress dupes without linear scans of the timeline! 2015-12-14 11:35:50 +00:00
Kegan Dougal 13cab79e04 Revert prev commit - emit SYNCING>SYNCING and now comment why we do this 2015-12-14 10:31:27 +00:00
Kegan Dougal fc6ce20e14 Check unsigned.age for getAge() for v2. Don't spam SYNCING emissions. 2015-12-14 10:27:53 +00:00
Kegan Dougal 9c49d26525 Linting 2015-12-14 09:24:33 +00:00
Kegan Dougal d6299b634c Scope filter keys in localStorage on user_id 2015-12-14 09:22:02 +00:00
Kegan Dougal a6f64b5f03 v2 filter test 2015-12-11 15:27:40 +00:00
Kegan Dougal 465635444f s/user_id/sender/g in tests 2015-12-11 15:07:40 +00:00
Kegan Dougal eedff29acb Add filter stub to crypto test 2015-12-11 13:35:46 +00:00
Kegan Dougal 7c43d15ea5 More linting; crypto test fix 2015-12-11 13:31:26 +00:00
Kegan Dougal de32ac0c44 Fix linting 2015-12-11 13:23:46 +00:00
Kegan Dougal 3d9d31d6b1 Fix remaining integration tests 2015-12-11 13:22:27 +00:00
Kegan Dougal b219836b3e Fix a bunch of integration tests 2015-12-11 12:53:26 +00:00
Kegan Dougal 26d9fed537 Fix MatrixClient unit tests 2015-12-11 11:07:31 +00:00
Kegan Dougal d6ba39f292 More linting 2015-12-10 15:01:39 +00:00
Kegan Dougal 8576ebce8f Linting 2015-12-10 14:57:13 +00:00
Kegan Dougal f08152a1d8 Handle ephemeral and account_data events 2015-12-10 14:27:21 +00:00
Kegan Dougal 6af2197183 Process join rooms and add local timeouts to /sync
This actually works now, though there's a number of teething
issues which may be app-specific. That, and all the tests are
broken.
2015-12-10 14:14:56 +00:00
Kegan Dougal 4c7e6807d2 Parse invites from /sync 2015-12-10 13:26:50 +00:00
Kegan Dougal 11f0513c62 Merge branch 'develop' into kegan/v2-sync 2015-12-10 11:50:48 +00:00
Kegan Dougal 3d57b4ce6a Be paranoid on /sync processing 2015-12-09 16:41:36 +00:00
Kegan Dougal 243bdd78f4 Handle presence key in /sync 2015-12-09 16:09:47 +00:00
Kegan Dougal b622960b32 Do all prep for /sync calls
This includes managing filters in localStorage. The /sync response
is not yet parsed.
2015-12-09 15:25:09 +00:00
Kegan Dougal 06f927aa22 Minor cleanup and reshuffle 2015-12-09 11:27:03 +00:00
Kegan Dougal f7ffed4b98 More linting 2015-12-09 11:19:20 +00:00
Kegan Dougal 0576e4ca0c Linting 2015-12-09 11:16:46 +00:00
Kegan Dougal 529fb23555 Linting 2015-12-09 10:43:21 +00:00
Kegan Dougal 3543abf7bd Make things work again 2015-12-09 10:42:38 +00:00
Kegsay a5847485b9 Merge pull request #52 from matrix-org/kegan/v2-filters
Support v2 filters
2015-12-09 09:10:59 +00:00
Kegan Dougal 2b659656cc Move alllll the sync code to sync.js - still more to do (in FIXME XXX) 2015-12-08 17:41:09 +00:00
Kegan Dougal ac3aa5538f Linting 2015-12-08 16:10:52 +00:00
Kegan Dougal c65f32f6a6 Add filter integration tests; more bug fixes. 2015-12-08 16:08:04 +00:00
Kegan Dougal 86a162c818 Add filter UTs and fix bugs 2015-12-08 15:39:55 +00:00
Kegan Dougal 1987726a95 Add initial v2 filter impl 2015-12-08 15:23:09 +00:00
Kegsay d2537cd00c Merge pull request #51 from matrix-org/kegan/unsent-timeline
Add config option to control how pending events are ordered
2015-12-07 17:00:43 +00:00
Kegan Dougal 61db191835 Add UTs 2015-12-07 15:45:13 +00:00
Kegan Dougal b7ac6a2e33 Add config option to sort pending events to the end of the timeline 2015-12-07 15:36:32 +00:00
Kegan Dougal c0178c3e80 Add getScheduler. Fix JSDoc 2015-12-07 11:29:02 +00:00
Kegsay e58fb29722 Merge pull request #50 from matrix-org/kegan/scrollback-requests
Scrollback improvements
2015-12-04 17:39:22 +00:00
Kegan Dougal a1300ec095 Wait for the last request (/messages, not /events) 2015-12-04 17:34:56 +00:00
Kegan Dougal 73e0216f78 Scrollback improvements
Add a 3s delay between scrollback requests if the previous scrollback request
failed.

Return the same promise if scrollback() is called multiple times whilst a
scrollback request is ongoing.
2015-12-04 17:27:16 +00:00
Kegan Dougal d16dfdaee3 Also emit a 'RoomState.members' event for m.room.power_levels 2015-12-04 16:11:13 +00:00
Kegan Dougal 02a605f368 Guest room ID fixes / initialSync support 2015-12-04 15:31:07 +00:00
Kegan Dougal 71d5756223 Fix linting 2015-12-04 09:36:18 +00:00
Kegan Dougal 2866743ce6 Fix broken test 2015-12-04 09:33:53 +00:00
David Baker e91a5e3793 Merge pull request #49 from matrix-org/upload_cancel
Add ability to cancel file uploads
2015-12-03 10:57:37 +00:00
David Baker 7f5ad041cc Pass the http status out with the error so upper level can can see what went wrong. 2015-12-03 10:51:09 +00:00
David Baker 92ea275275 Add ability to cancel file uploads 2015-12-02 18:14:13 +00:00
Kegan Dougal 0c114a2ab3 Only send up room_ids query param if isGuest is set 2015-12-02 12:34:25 +00:00
Matthew Hodgson e3757880ee add sendHtmlEmote 2015-11-29 01:18:22 +00:00
Kegan Dougal 88d680ef77 s/private_user_data/account_data/g 2015-11-20 17:32:25 +00:00
David Baker e1b3cf027c Merge pull request #47 from matrix-org/ignore-non-mxc
Don't return non-mxc URLs by default.
2015-11-12 15:51:20 +00:00
David Baker d0a725d1cc moar backticks 2015-11-12 15:50:57 +00:00
David Baker 07dbd26ba4 Typo 2015-11-12 15:40:59 +00:00
David Baker c93f56e4ec oops, 0.3.0 was already released 2015-11-12 15:40:32 +00:00
David Baker c0866b9787 Add changelog entry for breaking change 2015-11-12 15:39:12 +00:00
David Baker 14a9f6c444 lint & quote style 2015-11-12 12:14:13 +00:00
David Baker 588870b479 lint 2015-11-12 12:08:20 +00:00
David Baker f74bb3c145 Update UTs 2015-11-12 12:05:06 +00:00
David Baker 7095753410 Don't return non-mxc URLs by default. 2015-11-12 11:57:53 +00:00
Kegan Dougal 4f851dc431 Add isGuest/setGuest 2015-11-11 13:40:06 +00:00
Kegsay e89cc336b1 Merge pull request #46 from matrix-org/kegan/anon
Guest access endpoints
2015-11-10 17:06:04 +00:00
Kegan Dougal 959c588658 Guest rooms UTs 2015-11-10 16:49:50 +00:00
Kegan Dougal 7c887c1a5d Linting 2015-11-10 16:38:15 +00:00
Kegan Dougal 56bcf9796a Add MatrixClient.setGuestRooms
This is used in /events to grab events for the rooms the Guest is interested
in.
2015-11-10 16:36:44 +00:00
Kegan Dougal ee270314f8 Expose setting the HTTP body for extensibility 2015-11-10 16:30:41 +00:00
Kegan Dougal bda76afe4b Add a registerGuest endpoint 2015-11-10 16:25:15 +00:00
David Baker 4d426a3f31 The ts, not the event 2015-11-10 11:58:59 +00:00
Kegsay 9d33248c6e Merge pull request #45 from matrix-org/3pid-invites
Invite by 3PID endpoint
2015-11-09 17:09:35 +00:00
Kegan Dougal 46329ceb94 Remove the ability to set display_name in line with new spec 2015-11-09 16:58:52 +00:00
Kegan Dougal 2160f0bc08 Don't allow the IS URL to be configured on a per-request basis. 2015-11-09 16:51:12 +00:00
Kegan Dougal b231f19ec6 Make the display_name check for contains rather than equality. Add UT. 2015-11-09 16:50:10 +00:00
Kegan Dougal 6eb896e7a3 Use a small timeout when trying to poll when having previously failed. This makes re-connects propagate more quickly. 2015-11-09 15:55:09 +00:00
David Baker d7874315c3 Merge pull request #44 from matrix-org/implicit_read_receipts_2
Synthesize implicit read receipts in recalculateRoom
2015-11-09 15:08:04 +00:00
David Baker c95b27683f Add higher level keys to fake receipts 2015-11-09 15:05:46 +00:00
Kegan Dougal b0655d0431 Add UTs 2015-11-09 14:45:17 +00:00
David Baker ad24596d3f Revert c13b180 as it fails lint (creating functions in a loop) 2015-11-09 13:48:05 +00:00
Kegan Dougal 80a6cf34e2 Add 3pid invite endpoints 2015-11-09 11:58:45 +00:00
David Baker c13b1800b9 forEach probably nicer here 2015-11-09 10:23:37 +00:00
Kegsay b4bb0f011d Merge pull request #39 from matrix-org/matthew/room-tags
Room tag support
2015-11-09 09:34:57 +00:00
Matthew Hodgson b9ace61ccb split long lines 2015-11-07 20:26:16 +00:00
Matthew Hodgson 21273582a4 room tagging unit tests 2015-11-07 20:23:21 +00:00
Matthew Hodgson 53bbabea4f pass event in the Room.tags event 2015-11-07 20:23:09 +00:00
Matthew Hodgson 170a78a420 convert PUD POJOs to events with ugly utils.map rather than iterating in the for loop 2015-11-07 20:20:50 +00:00
Matthew Hodgson 8771ced8e4 fix jsdoc thinko 2015-11-07 17:28:37 +00:00
Matthew Hodgson dc7d2698b7 Merge branch 'develop' into matthew/room-tags 2015-11-07 17:25:53 +00:00
Matthew Hodgson 3d4694a92f fix casing of tagName 2015-11-07 17:22:45 +00:00
David Baker 8b2f94a6b2 Merge branch 'develop' into implicit_read_receipts_2 2015-11-06 15:39:16 +00:00
David Baker 6736164d98 Make linter happy (space at end of line) 2015-11-06 15:38:46 +00:00
David Baker 77266fe221 Fix lint errors and make thing that didn't need to be a member function not a member function 2015-11-06 15:26:35 +00:00
David Baker 14a48c1182 Synthesize implicit read receipts in recalculateRoom to make them correct when the room is first loaded. 2015-11-06 15:13:30 +00:00
Kegsay 5f6e52f367 Merge pull request #42 from stevenhammerton/sh-token-login
SH - CAS / Login token login
2015-11-06 14:34:48 +00:00
Steven Hammerton e71a87c62c Update javadoc 2015-11-06 12:14:24 +00:00
Steven Hammerton b963f177cc Update CAS login to return url rather than update location as the JS SDK may not be run within a browser env 2015-11-06 12:11:50 +00:00
Steven Hammerton c3097979f2 Change login with CAS to redirect to HS for CAS login 2015-11-06 11:19:32 +00:00
Kegan Dougal 21e56d2f53 Tweak RETRY_BACKOFF_RATELIMIT to take browser-request's CORS failures into account. 2015-11-05 15:48:48 +00:00
Kegsay 455ce26741 Merge pull request #40 from matrix-org/kegan/syncing
Syncing bugs/fixes
2015-11-05 14:53:47 +00:00
Steven Hammerton d241f5b3eb Add login with token method 2015-11-05 14:51:23 +00:00
David Baker d34f8eda1a Merge pull request #41 from matrix-org/implicit_read_receipts
Implicit read receipts
2015-11-05 14:42:48 +00:00
David Baker 483095c3da Fix PR comments 2015-11-05 14:41:35 +00:00
David Baker 856c34016d Fix event removal 2015-11-05 14:13:52 +00:00
David Baker ad80d4f059 fix lint errors 2015-11-05 13:57:21 +00:00
David Baker 0da547a239 Implicit read receipts
* Inject implicit read receipts into the timeline
 * Twiddle local echo a bit to make the implicit receipts match the various different stages of local echo.
2015-11-05 13:39:03 +00:00
Kegan Dougal 16278892d8 Modify how detection of the end of pagination is done
Synapse may filter down the events resulting in < 'limit' events being
returned *but it still has more events*. Change the check to see if the
request returned an empty array instead. This may add an extra HTTP hit.
2015-11-05 13:26:16 +00:00
Kegan Dougal 8500f404a9 Finish implementing UTs 2015-11-05 13:12:37 +00:00
Kegan Dougal 5d782a317c Add some sync emission tests. Emit after starting timers.
We want to emit AFTER starting the timers so tests can speed
up time. We also want to do this because clients may want to
retryImmediately() on sync errors (which would be lost unless
the timer had already been started)
2015-11-04 16:09:30 +00:00
Kegan Dougal af435204a0 More helpful logging 2015-11-04 15:40:42 +00:00
Kegan Dougal b4c353e65f Linting 2015-11-04 15:37:10 +00:00
Kegan Dougal e42f6c0cad Add http fixings to allow MatrixClient UTs 2015-11-04 15:35:31 +00:00
David Baker bc512a6e4c Check m.room.name event actually has a name in the content before using it. This should fix the recent disasters with #android being shown as 'undefined' (or crashing vector). 2015-11-04 15:20:25 +00:00
Kegsay 9cf7edc48d Merge pull request #38 from matrix-org/receipt_events
Emit events for read receipts
2015-11-04 12:04:19 +00:00
David Baker 904539df58 Fix c+p fail & add unit test 2015-11-04 12:02:02 +00:00
Kegan Dougal c9df9c33a8 Linting 2015-11-04 11:53:10 +00:00
Kegan Dougal 5c3bfa6a83 Add stub unit tests for syncing 2015-11-04 11:50:32 +00:00
Matthew Hodgson 149ed04a4f fix some review feedback; add initial api for setting & deleting tags; still a WIP 2015-11-04 02:24:36 +00:00
Kegan Dougal e98eaaee6e Add MatrixClient.retryImmediately() to stop backing off and sync RIGHT NOW 2015-11-03 17:13:50 +00:00
Kegan Dougal 4b93d801ae Implement the new sync state API
Also have retry schemes for the rest of the syncing ops (/events, /pushrules)
2015-11-03 16:44:19 +00:00
Matthew Hodgson 5a1cc4c2e7 store the tags in the right place 2015-11-03 16:19:52 +00:00
Matthew Hodgson 8016a70bc4 remember to check initialSync for m.tag events 2015-11-03 16:18:12 +00:00
Matthew Hodgson 70536d5676 add support for tracking room tags 2015-11-03 16:05:48 +00:00
Kegan Dougal 27ce0970c5 BREAKING: Introduce a formal API for syncing state
BREAKING CHANGE:
  This replaces syncComplete and syncError.
2015-11-03 14:35:49 +00:00
Kegan Dougal 49f6634d73 Retry /initialSync if it fails (exp backoff up to 2.1min). 2015-11-03 14:01:17 +00:00
David Baker 142ee81e66 Emit events for read receipts 2015-11-03 11:43:52 +00:00
Kegan Dougal 3b21998d96 Expose timeout= on /events to clients 2015-11-03 10:18:56 +00:00
Kegan Dougal 0fb307d09b Use the history length specified in startClient() for room initial syncs. 2015-11-03 10:15:30 +00:00
Kegsay c1160d3419 Merge pull request #36 from matrix-org/kegan/event-stream-js-errors
Wrap /events response processing in a try/catch
2015-11-02 17:13:17 +00:00
Kegsay 48253f0ff0 Merge pull request #37 from matrix-org/kegan/supports-voip
Add MatrixClient.supportsVoip()
2015-11-02 17:10:46 +00:00
Kegan Dougal c3c7ee5453 Add MatrixClient.supportsVoip()
This allows developers to gracefully degrade their UIs if VoIP is not supported.
2015-11-02 17:03:19 +00:00
David Baker c6aac8cbd9 Merge pull request #35 from matrix-org/event_read_up_to
Add event to get last read receipt for a user.
2015-11-02 16:04:16 +00:00
David Baker 1b43bc78d0 Remove unnecessary null check & s/"/'/ 2015-11-02 16:02:48 +00:00
Kegan Dougal 93a091c7e8 Wrap /events response processing in a try/catch 2015-11-02 16:02:06 +00:00
David Baker 083dde3557 Fix doc 2015-11-02 16:00:40 +00:00
David Baker 4adc5f2c85 Also need to check if the event is null 2015-11-02 15:19:29 +00:00
Daniel Wagner-Hall ced14819e4 Merge pull request #34 from matrix-org/daniel/naming
Simplify logic and layout
2015-11-02 15:07:58 +00:00
Daniel Wagner-Hall 0b42d85c5b Use double-quotes for consistency 2015-11-02 15:07:47 +00:00
David Baker c4a35020f1 Add event to get last read receipt for a user. 2015-11-02 14:39:10 +00:00
Daniel Wagner-Hall 11f052bcc6 Simplify logic and layout 2015-10-30 14:58:59 +00:00
Kegan Dougal 0ea11ea806 Add 0.3.0 browser dist 2015-10-28 16:54:07 +00:00
Kegan Dougal 7cad5a0479 Merge branch 'develop' 2015-10-28 16:49:11 +00:00
Kegan Dougal 83c53f6a79 Fix doc typo 2015-10-28 16:48:08 +00:00
Kegan Dougal ae13ed7ded Add disclaimer to screensharing 2015-10-28 16:45:07 +00:00
Kegan Dougal b17385120a Bump to 0.3.0 and add CHANGELOG 2015-10-28 16:42:44 +00:00
Kegsay cc0d8da416 Merge pull request #32 from matrix-org/member-info-for-invites
Retrieving profile info for invites
2015-10-26 16:42:21 +00:00
Kegsay c796702eba Merge pull request #31 from matrix-org/search-api
Add search functions and tests
2015-10-26 16:36:15 +00:00
Kegan Dougal 2675442ced Line lengths 2015-10-26 16:31:10 +00:00
Kegan Dougal aa3e6514c6 Add test for firing (pew pew) of events 2015-10-26 16:30:15 +00:00
Kegan Dougal be6d64fbfd Add integration tests; fix bugs. 2015-10-26 16:12:06 +00:00
Kegan Dougal 4cbab72369 Resolve invites to profile info
This is so inviters/invitees have a display name and avatar_url if they have
set one. This info isn't contained in the m.room.member event so we get it
direct from /profile.

This is gated behind `resolveInvitesToProfiles` on `startClient(opts)`.
2015-10-26 15:27:44 +00:00
Kegan Dougal 0227b1c68d Add search functions and tests 2015-10-26 13:27:45 +00:00
Matthew Hodgson 4c051202af s/getMembersWithMemership/getMembersWithMembership/g 2015-10-24 01:45:02 +01:00
Matthew Hodgson 981b9e0595 Merge branch 'screen-sharing' into develop 2015-10-23 12:58:55 +01:00
Matthew Hodgson 9e719ba31e drop res back to 640x360 as 1024x576 gave us the wrong aspect ratio 2015-10-23 12:57:46 +01:00
Kegan Dougal c65f576f8d More logging 2015-10-21 17:15:26 +01:00
Kegan Dougal 2c805bbece More paranoia when handling responses 2015-10-21 16:00:31 +01:00
Kegan Dougal 02b836698c More clarity on cache updating 2015-10-21 14:07:14 +01:00
Kegsay 25112ede58 Merge pull request #30 from matrix-org/set-state-events-perf
Change calculating display names from O(n^2) to O(n)
2015-10-21 13:47:54 +01:00
Kegan Dougal 5888c8a56c Commenting on splice 2015-10-21 13:47:23 +01:00
Kegan Dougal 1cee7bf397 JSDoc 2015-10-21 13:30:32 +01:00
Kegan Dougal cab7a71a94 Change calculating display names from O(n^2) to O(n)
Reduces initial sync times from ~30s to ~1s on accounts with heavily
populated rooms.

The problem was that f.e. RoomMember it would try to calculate the
display name, which involved looping each RoomMember to get their
display name to check for disambiguation. We now cache display names
to user IDs so we don't need to loop every member when disambiguating.
2015-10-21 13:25:23 +01:00
Matthew Hodgson d7c63e3487 Merge pull request #29 from matrix-org/screen-sharing
Basic screen-sharing support; adds a screensharing stream into the call and adds support for playback of both an audio-only stream (i.e. the voice-over) alongside an AV stream like the screenshare.
2015-10-21 01:45:38 +01:00
Matthew Hodgson bff749fd50 fix linter 2015-10-21 01:44:54 +01:00
Matthew Hodgson 5c286352cb improve constraints a bit; fix comments; try to stop sharing more aggressively 2015-10-21 01:40:20 +01:00
Matthew Hodgson 9ec3504c72 dial down logging 2015-10-21 01:40:01 +01:00
Matthew Hodgson 26b3e32ca2 add the concept of a dedicated remote audio element used for playing back audio-only streams (i.e. voice calls, and the voice stream that accompanies a screenshare). Correctly tidy up screen capture calls. 2015-10-21 01:18:55 +01:00
Kegan Dougal 4e2c83cc08 Debug logging 2015-10-20 17:21:25 +01:00
Kegan Dougal 17def14eba Get screen-sharing with audio working 2015-10-20 16:43:51 +01:00
Kegan Dougal f260de573b Add right constraints to get screen-sharing working
Requires --enable-usermedia-screen-capturing flag on chrome enabled.
2015-10-20 15:11:17 +01:00
David Baker 4fd45ab278 Merge pull request #28 from matrix-org/voip-mute
VoIP local muting
2015-10-20 10:33:24 +01:00
Kegsay 4a2e9eb927 Merge pull request #27 from matrix-org/room-avatars
Room avatars
2015-10-19 16:51:30 +01:00
Kegan Dougal dd8adef9ed Remove unused args 2015-10-19 16:50:16 +01:00
Kegan Dougal 9164debf03 Add the same for video 2015-10-19 16:48:47 +01:00
Kegan Dougal 534bef8632 Add MatrixCall.isMicrophoneMuted() 2015-10-19 16:28:01 +01:00
Kegan Dougal d8c43d02ba Add MatrixCall.setMicrophoneMuted 2015-10-19 16:21:13 +01:00
Kegsay ae3738f822 Formatting 2015-10-19 15:39:23 +01:00
Kegan Dougal be621e1aa7 Add breaking changes to CHANGELOG 2015-10-19 15:38:39 +01:00
Kegan Dougal 343d63a28a Merge branch 'develop' into room-avatars 2015-10-19 15:33:42 +01:00
Kegsay 0a28d6e950 Merge pull request #26 from matrix-org/invite-room-state
Invite room state
2015-10-19 15:31:45 +01:00
Kegsay b493a62afa Merge pull request #25 from matrix-org/initial-sync-improvements
Add support for archived=true in initial sync
2015-10-19 15:31:32 +01:00
Kegsay 37a8c9bd72 Merge pull request #23 from matrix-org/read_receipts
Receipts
2015-10-19 15:30:23 +01:00
Kegan Dougal a9c4345159 Clarify the link is the source of the code 2015-10-19 15:29:57 +01:00
Kegsay 5f1153b43f Merge pull request #24 from matrix-org/canonical-alias
Look for a canonical alias when determining the room name
2015-10-19 15:28:33 +01:00
Kegan Dougal 2c213f88d9 Units! Tests! Linting! 2015-10-19 15:24:24 +01:00
Kegan Dougal a236219111 ContentRepo unit tests 2015-10-19 15:00:06 +01:00
Kegan Dougal 2f9958cca9 JSDoc linkify 2015-10-19 14:37:17 +01:00
Kegan Dougal f26154d0ac Add support for m.room.avatar: refactor avatar URLs
BREAKING CHANGE.

Scope each "getAvatarUrl" to be instance methods on the entity it
relates to (Room and RoomMember respectively). By doing this, we
can actually pull out specific state such as the `m.room.avatar`
event more easily rather than keeping it in the global cesspit
of `MatrixClient`.

This was complicated by `getHttpUriForMxc` and `getIdenticonUri`
which were attached to the HTTP API to pull out the `baseUrl` when
crafting the URL. Pull out this dependency out and explicitly pass
it in when crafting the URL. This is trivial to get from
`MatrixClient.getHomeserverUrl()`.
2015-10-19 14:14:34 +01:00
Kegan Dougal 5ae87b7c95 Bug fixes and unit tests 2015-10-16 17:27:05 +01:00
Kegan Dougal 219103a4e2 Yank out invite event from initialSync. Set stripped state events when recalculating invited rooms. 2015-10-16 17:07:04 +01:00
Kegan Dougal 4ec7b9bb3f Add support for archived=true in initial sync
Make MatrixClient.startClient take 'opts' instead of 'historyLen' in
a backwards compatible way. Add 'includeArchivedRooms' as an option.
2015-10-16 15:00:26 +01:00
Kegan Dougal bad8b7fb76 Look for a canonical alias when determining the room name 2015-10-16 14:30:21 +01:00
Kegan Dougal a101857cb6 Add integration tests for read receipts 2015-10-16 13:51:44 +01:00
Kegan Dougal a52f92830a Implement unit tests for read receipts. 2015-10-16 13:37:53 +01:00
Kegan Dougal 40d113a423 Pass in receipts from initialSync 2015-10-16 11:54:47 +01:00
Kegan Dougal 7ec8421d19 Fix linting errors 2015-10-16 11:38:49 +01:00
Kegan Dougal 9048efeb65 Implement receipt handling and expose new Room functions
Add polyfills for Array.map/filter according to MDN because it looks much
better than the utils format.

Add stub tests for edge cases and implement test for the common case.
2015-10-16 11:32:27 +01:00
Kegan Dougal 43fc200dae Read receipt HTTP API tweaks 2015-10-16 09:36:13 +01:00
David Baker 6679e93afc Add untested read receipt sending method 2015-10-16 09:12:50 +01:00
89 changed files with 21949 additions and 35048 deletions
+5 -1
View File
@@ -6,4 +6,8 @@ coverage
lib-cov
out
reports
dist/browser-matrix-dev.js
/dist
# version file and tarball created by 'npm pack'
/git-revision.txt
/matrix-js-sdk-*.tgz
+1 -1
View File
@@ -5,7 +5,7 @@
"nonew": true,
"curly": true,
"forin": true,
"freeze": true,
"freeze": false,
"undef": true,
"unused": "vars"
}
+3
View File
@@ -0,0 +1,3 @@
language: node_js
node_js:
- node # Latest stable version of nodejs.
+585 -7
View File
@@ -1,3 +1,581 @@
Changes in [0.7.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.2) (2016-12-15)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.1...v0.7.2)
* Bump to Olm 2.0
[\#309](https://github.com/matrix-org/matrix-js-sdk/pull/309)
* Sanity check payload length before encrypting
[\#307](https://github.com/matrix-org/matrix-js-sdk/pull/307)
* Remove dead _sendPingToDevice function
[\#308](https://github.com/matrix-org/matrix-js-sdk/pull/308)
* Add setRoomDirectoryVisibilityAppService
[\#306](https://github.com/matrix-org/matrix-js-sdk/pull/306)
* Update release script to do signed releases
[\#305](https://github.com/matrix-org/matrix-js-sdk/pull/305)
* e2e: Wait for pending device lists
[\#304](https://github.com/matrix-org/matrix-js-sdk/pull/304)
* Start a new megolm session when devices are blacklisted
[\#303](https://github.com/matrix-org/matrix-js-sdk/pull/303)
* E2E: Download our own devicelist on startup
[\#302](https://github.com/matrix-org/matrix-js-sdk/pull/302)
Changes in [0.7.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.1) (2016-12-09)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.1-rc.1...v0.7.1)
No changes
Changes in [0.7.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.1-rc.1) (2016-12-05)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.0...v0.7.1-rc.1)
* Avoid NPE when no sessionStore is given
[\#300](https://github.com/matrix-org/matrix-js-sdk/pull/300)
* Improve decryption error messages
[\#299](https://github.com/matrix-org/matrix-js-sdk/pull/299)
* Revert "Use native Array.isArray when available."
[\#283](https://github.com/matrix-org/matrix-js-sdk/pull/283)
* Use native Array.isArray when available.
[\#282](https://github.com/matrix-org/matrix-js-sdk/pull/282)
Changes in [0.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.0) (2016-11-18)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4...v0.7.0)
* Avoid a packetstorm of device queries on startup
[\#297](https://github.com/matrix-org/matrix-js-sdk/pull/297)
* E2E: Check devices to share keys with on each send
[\#295](https://github.com/matrix-org/matrix-js-sdk/pull/295)
* Apply unknown-keyshare mitigations
[\#296](https://github.com/matrix-org/matrix-js-sdk/pull/296)
* distinguish unknown users from deviceless users
[\#294](https://github.com/matrix-org/matrix-js-sdk/pull/294)
* Allow starting client with initialSyncLimit = 0
[\#293](https://github.com/matrix-org/matrix-js-sdk/pull/293)
* Make timeline-window _unpaginate public and rename to unpaginate
[\#289](https://github.com/matrix-org/matrix-js-sdk/pull/289)
* Send a STOPPED sync updated after call to stopClient
[\#286](https://github.com/matrix-org/matrix-js-sdk/pull/286)
* Fix bug in verifying megolm event senders
[\#292](https://github.com/matrix-org/matrix-js-sdk/pull/292)
* Handle decryption of events after they arrive
[\#288](https://github.com/matrix-org/matrix-js-sdk/pull/288)
* Fix examples.
[\#287](https://github.com/matrix-org/matrix-js-sdk/pull/287)
* Add a travis.yml
[\#278](https://github.com/matrix-org/matrix-js-sdk/pull/278)
* Encrypt all events, including 'm.call.*'
[\#277](https://github.com/matrix-org/matrix-js-sdk/pull/277)
* Ignore reshares of known megolm sessions
[\#276](https://github.com/matrix-org/matrix-js-sdk/pull/276)
* Log to the console on unknown session
[\#274](https://github.com/matrix-org/matrix-js-sdk/pull/274)
* Make it easier for SDK users to wrap prevailing the 'request' function
[\#273](https://github.com/matrix-org/matrix-js-sdk/pull/273)
Changes in [0.6.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4) (2016-11-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4-rc.2...v0.6.4)
* Change release script to pass version by environment variable
Changes in [0.6.4-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4-rc.2) (2016-11-02)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4-rc.1...v0.6.4-rc.2)
* Add getRoomTags method to client
[\#236](https://github.com/matrix-org/matrix-js-sdk/pull/236)
Changes in [0.6.4-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4-rc.1) (2016-11-02)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.3...v0.6.4-rc.1)
Breaking Changes
----------------
* Bundled version of the JS SDK are no longer versioned along with
source files in the dist/ directory. As of this release, they
will be included in the release tarball, but not the source
repository.
Other Changes
-------------
* More fixes to the release script
[\#272](https://github.com/matrix-org/matrix-js-sdk/pull/272)
* Update the release process to use github releases
[\#271](https://github.com/matrix-org/matrix-js-sdk/pull/271)
* Don't package the world when we release
[\#270](https://github.com/matrix-org/matrix-js-sdk/pull/270)
* Add ability to set a filter prior to the first /sync
[\#269](https://github.com/matrix-org/matrix-js-sdk/pull/269)
* Sign one-time keys, and verify their signatures
[\#243](https://github.com/matrix-org/matrix-js-sdk/pull/243)
* Check for duplicate message indexes for group messages
[\#241](https://github.com/matrix-org/matrix-js-sdk/pull/241)
* Rotate megolm sessions
[\#240](https://github.com/matrix-org/matrix-js-sdk/pull/240)
* Check recipient and sender in Olm messages
[\#239](https://github.com/matrix-org/matrix-js-sdk/pull/239)
* Consistency checks for E2E device downloads
[\#237](https://github.com/matrix-org/matrix-js-sdk/pull/237)
* Support User-Interactive auth for delete device
[\#235](https://github.com/matrix-org/matrix-js-sdk/pull/235)
* Utility to help with interactive auth
[\#234](https://github.com/matrix-org/matrix-js-sdk/pull/234)
* Fix sync breaking when an invalid filterId is in localStorage
[\#228](https://github.com/matrix-org/matrix-js-sdk/pull/228)
Changes in [0.6.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.3) (2016-10-12)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.2...v0.6.3)
Breaking Changes
----------------
* Add a 'RECONNECTING' state to the sync states. This is an additional state
between 'SYNCING' and 'ERROR', so most clients should not notice.
Other Changes
----------------
* Fix params getting replaced on register calls
[\#233](https://github.com/matrix-org/matrix-js-sdk/pull/233)
* Fix potential 30s delay on reconnect
[\#232](https://github.com/matrix-org/matrix-js-sdk/pull/232)
* uploadContent: Attempt some consistency between browser and node
[\#230](https://github.com/matrix-org/matrix-js-sdk/pull/230)
* Fix error handling on uploadContent
[\#229](https://github.com/matrix-org/matrix-js-sdk/pull/229)
* Fix uploadContent for node.js
[\#226](https://github.com/matrix-org/matrix-js-sdk/pull/226)
* Don't emit ERROR until a keepalive poke fails
[\#223](https://github.com/matrix-org/matrix-js-sdk/pull/223)
* Function to get the fallback url for interactive auth
[\#224](https://github.com/matrix-org/matrix-js-sdk/pull/224)
* Revert "Handle the first /sync failure differently."
[\#222](https://github.com/matrix-org/matrix-js-sdk/pull/222)
Changes in [0.6.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.2) (2016-10-05)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.1...v0.6.2)
* Check dependencies aren't on develop in release.sh
[\#221](https://github.com/matrix-org/matrix-js-sdk/pull/221)
* Fix checkTurnServers leak on logout
[\#220](https://github.com/matrix-org/matrix-js-sdk/pull/220)
* Fix leak of file upload objects
[\#219](https://github.com/matrix-org/matrix-js-sdk/pull/219)
* crypto: remove duplicate code
[\#218](https://github.com/matrix-org/matrix-js-sdk/pull/218)
* Add API for 3rd party location lookup
[\#217](https://github.com/matrix-org/matrix-js-sdk/pull/217)
* Handle the first /sync failure differently.
[\#216](https://github.com/matrix-org/matrix-js-sdk/pull/216)
Changes in [0.6.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.1) (2016-09-21)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.0...v0.6.1)
* Fix the ed25519 key checking
[\#215](https://github.com/matrix-org/matrix-js-sdk/pull/215)
* Add MatrixClient.getEventSenderDeviceInfo()
[\#214](https://github.com/matrix-org/matrix-js-sdk/pull/214)
Changes in [0.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.0) (2016-09-21)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.6...v0.6.0)
* Pull user device list on join
[\#212](https://github.com/matrix-org/matrix-js-sdk/pull/212)
* Fix sending of oh_hais on bad sessions
[\#213](https://github.com/matrix-org/matrix-js-sdk/pull/213)
* Support /publicRooms pagination
[\#211](https://github.com/matrix-org/matrix-js-sdk/pull/211)
* Update the olm library version to 1.3.0
[\#205](https://github.com/matrix-org/matrix-js-sdk/pull/205)
* Comment what the logic in uploadKeys does
[\#209](https://github.com/matrix-org/matrix-js-sdk/pull/209)
* Include keysProved and keysClaimed in the local echo for events we send.
[\#210](https://github.com/matrix-org/matrix-js-sdk/pull/210)
* Check if we need to upload new one-time keys every 10 minutes
[\#208](https://github.com/matrix-org/matrix-js-sdk/pull/208)
* Reset oneTimeKey to null on each loop iteration.
[\#207](https://github.com/matrix-org/matrix-js-sdk/pull/207)
* Add getKeysProved and getKeysClaimed methods to MatrixEvent.
[\#206](https://github.com/matrix-org/matrix-js-sdk/pull/206)
* Send a 'm.new_device' when we get a message for an unknown group session
[\#204](https://github.com/matrix-org/matrix-js-sdk/pull/204)
* Introduce EventTimelineSet, filtered timelines and global notif timeline.
[\#196](https://github.com/matrix-org/matrix-js-sdk/pull/196)
* Wrap the crypto event handlers in try/catch blocks
[\#203](https://github.com/matrix-org/matrix-js-sdk/pull/203)
* Show warnings on to-device decryption fail
[\#202](https://github.com/matrix-org/matrix-js-sdk/pull/202)
* s/Displayname/DisplayName/
[\#201](https://github.com/matrix-org/matrix-js-sdk/pull/201)
* OH HAI
[\#200](https://github.com/matrix-org/matrix-js-sdk/pull/200)
* Share the current ratchet with new members
[\#199](https://github.com/matrix-org/matrix-js-sdk/pull/199)
* Move crypto bits into a subdirectory
[\#198](https://github.com/matrix-org/matrix-js-sdk/pull/198)
* Refactor event handling in Crypto
[\#197](https://github.com/matrix-org/matrix-js-sdk/pull/197)
* Don't create Olm sessions proactively
[\#195](https://github.com/matrix-org/matrix-js-sdk/pull/195)
* Use to-device events for key sharing
[\#194](https://github.com/matrix-org/matrix-js-sdk/pull/194)
* README: callbacks deprecated
[\#193](https://github.com/matrix-org/matrix-js-sdk/pull/193)
* Fix sender verification for megolm messages
[\#192](https://github.com/matrix-org/matrix-js-sdk/pull/192)
* Use `ciphertext` instead of `body` in megolm events
[\#191](https://github.com/matrix-org/matrix-js-sdk/pull/191)
* Add debug methods to get the state of OlmSessions
[\#189](https://github.com/matrix-org/matrix-js-sdk/pull/189)
* MatrixClient.getStoredDevicesForUser
[\#190](https://github.com/matrix-org/matrix-js-sdk/pull/190)
* Olm-related cleanups
[\#188](https://github.com/matrix-org/matrix-js-sdk/pull/188)
* Update to fixed olmlib
[\#187](https://github.com/matrix-org/matrix-js-sdk/pull/187)
* always play audio out of the remoteAudioElement if it exists.
[\#186](https://github.com/matrix-org/matrix-js-sdk/pull/186)
* Fix exceptions where HTMLMediaElement loads and plays race
[\#185](https://github.com/matrix-org/matrix-js-sdk/pull/185)
* Reset megolm session when people join/leave the room
[\#183](https://github.com/matrix-org/matrix-js-sdk/pull/183)
* Fix exceptions when dealing with redactions
[\#184](https://github.com/matrix-org/matrix-js-sdk/pull/184)
Changes in [0.5.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.6) (2016-08-28)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.5...v0.5.6)
* Put all of the megolm keys in one room message
[\#182](https://github.com/matrix-org/matrix-js-sdk/pull/182)
* Reinstate device blocking for simple Olm
[\#181](https://github.com/matrix-org/matrix-js-sdk/pull/181)
* support for unpacking megolm keys
[\#180](https://github.com/matrix-org/matrix-js-sdk/pull/180)
* Send out megolm keys when we start a megolm session
[\#179](https://github.com/matrix-org/matrix-js-sdk/pull/179)
* Change the result structure for ensureOlmSessionsForUsers
[\#178](https://github.com/matrix-org/matrix-js-sdk/pull/178)
* Factor out a function for doing olm encryption
[\#177](https://github.com/matrix-org/matrix-js-sdk/pull/177)
* Move DeviceInfo and DeviceVerification to separate module
[\#175](https://github.com/matrix-org/matrix-js-sdk/pull/175)
* Make encryption asynchronous
[\#176](https://github.com/matrix-org/matrix-js-sdk/pull/176)
* Added ability to set and get status_msg for presence.
[\#167](https://github.com/matrix-org/matrix-js-sdk/pull/167)
* Megolm: don't dereference nullable object
[\#174](https://github.com/matrix-org/matrix-js-sdk/pull/174)
* Implement megolm encryption/decryption
[\#173](https://github.com/matrix-org/matrix-js-sdk/pull/173)
* Update our push rules when they come down stream
[\#170](https://github.com/matrix-org/matrix-js-sdk/pull/170)
* Factor Olm encryption/decryption out to new classes
[\#172](https://github.com/matrix-org/matrix-js-sdk/pull/172)
* Make DeviceInfo more useful, and refactor crypto methods to use it
[\#171](https://github.com/matrix-org/matrix-js-sdk/pull/171)
* Move login and register methods into base-apis
[\#169](https://github.com/matrix-org/matrix-js-sdk/pull/169)
* Remove defaultDeviceDisplayName from MatrixClient options
[\#168](https://github.com/matrix-org/matrix-js-sdk/pull/168)
Changes in [0.5.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.5) (2016-08-11)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.4...v0.5.5)
* Add room.getAliases() and room.getCanonicalAlias
* Add API calls `/register/email/requestToken`, `/account/password/email/requestToken` and `/account/3pid/email/requestToken`
* Add `User.currentlyActive` and `User.lastPresenceTs` events for changes in fields on the User object
* Add `logout` and `deactivateAccount`
* Make sure we actually stop the sync loop on logout
[\#166](https://github.com/matrix-org/matrix-js-sdk/pull/166)
* Zero is a valid power level
[\#164](https://github.com/matrix-org/matrix-js-sdk/pull/164)
* Verify e2e keys on download
[\#163](https://github.com/matrix-org/matrix-js-sdk/pull/163)
* Factor crypto stuff out of MatrixClient
[\#162](https://github.com/matrix-org/matrix-js-sdk/pull/162)
* Refactor device key upload
[\#161](https://github.com/matrix-org/matrix-js-sdk/pull/161)
* Wrappers for devices API
[\#158](https://github.com/matrix-org/matrix-js-sdk/pull/158)
* Add deactivate account function
[\#160](https://github.com/matrix-org/matrix-js-sdk/pull/160)
* client.listDeviceKeys: Expose device display name
[\#159](https://github.com/matrix-org/matrix-js-sdk/pull/159)
* Add `logout`
[\#157](https://github.com/matrix-org/matrix-js-sdk/pull/157)
* Fix email registration
[\#156](https://github.com/matrix-org/matrix-js-sdk/pull/156)
* Factor out MatrixClient methods to MatrixBaseApis
[\#155](https://github.com/matrix-org/matrix-js-sdk/pull/155)
* Fix some broken tests
[\#154](https://github.com/matrix-org/matrix-js-sdk/pull/154)
* make jenkins fail the build if the tests fail
[\#153](https://github.com/matrix-org/matrix-js-sdk/pull/153)
* deviceId-related fixes
[\#152](https://github.com/matrix-org/matrix-js-sdk/pull/152)
* /login, /register: Add device_id and initial_device_display_name
[\#151](https://github.com/matrix-org/matrix-js-sdk/pull/151)
* Support global account_data
[\#150](https://github.com/matrix-org/matrix-js-sdk/pull/150)
* Add more events to User
[\#149](https://github.com/matrix-org/matrix-js-sdk/pull/149)
* Add API calls for other requestToken endpoints
[\#148](https://github.com/matrix-org/matrix-js-sdk/pull/148)
* Add register-specific request token endpoint
[\#147](https://github.com/matrix-org/matrix-js-sdk/pull/147)
* Set a valid SPDX license identifier in package.json
[\#139](https://github.com/matrix-org/matrix-js-sdk/pull/139)
* Configure encryption on m.room.encryption events
[\#145](https://github.com/matrix-org/matrix-js-sdk/pull/145)
* Implement device blocking
[\#146](https://github.com/matrix-org/matrix-js-sdk/pull/146)
* Clearer doc for setRoomDirectoryVisibility
[\#144](https://github.com/matrix-org/matrix-js-sdk/pull/144)
* crypto: use memberlist to derive recipient list
[\#143](https://github.com/matrix-org/matrix-js-sdk/pull/143)
* Support for marking devices as unverified
[\#142](https://github.com/matrix-org/matrix-js-sdk/pull/142)
* Add Olm as an optionalDependency
[\#141](https://github.com/matrix-org/matrix-js-sdk/pull/141)
* Add room.getAliases() and room.getCanonicalAlias()
[\#140](https://github.com/matrix-org/matrix-js-sdk/pull/140)
* Change how MatrixEvent manages encrypted events
[\#138](https://github.com/matrix-org/matrix-js-sdk/pull/138)
* Catch exceptions when encrypting events
[\#137](https://github.com/matrix-org/matrix-js-sdk/pull/137)
* Support for marking devices as verified
[\#136](https://github.com/matrix-org/matrix-js-sdk/pull/136)
* Various matrix-client refactorings and fixes
[\#134](https://github.com/matrix-org/matrix-js-sdk/pull/134)
Changes in [0.5.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.4) (2016-06-02)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.3...v0.5.4)
* Correct fix for https://github.com/vector-im/vector-web/issues/1039
* Make release.sh work on OSX
Changes in [0.5.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.3) (2016-06-02)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.2...v0.5.3)
* Add support for the openid interface
[\#133](https://github.com/matrix-org/matrix-js-sdk/pull/133)
* Bugfix for HTTP upload content when running on node
[\#129](https://github.com/matrix-org/matrix-js-sdk/pull/129)
* Ignore missing profile (displayname and avatar_url) fields on
presence events, rather than overwriting existing valid profile
data from membership events or elsewhere.
Fixes https://github.com/vector-im/vector-web/issues/1039
Changes in [0.5.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.2) (2016-04-19)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.1...v0.5.2)
* Track the absolute time that presence events are received, so that the
relative lastActiveAgo value is meaningful.
[\#128](https://github.com/matrix-org/matrix-js-sdk/pull/128)
* Refactor the addition of events to rooms
[\#127](https://github.com/matrix-org/matrix-js-sdk/pull/127)
* Clean up test shutdown
[\#126](https://github.com/matrix-org/matrix-js-sdk/pull/126)
* Add methods to get (and set) pushers
[\#125](https://github.com/matrix-org/matrix-js-sdk/pull/125)
* URL previewing support
[\#122](https://github.com/matrix-org/matrix-js-sdk/pull/122)
* Avoid paginating forever in private rooms
[\#124](https://github.com/matrix-org/matrix-js-sdk/pull/124)
* Fix a bug where we recreated sync filters
[\#123](https://github.com/matrix-org/matrix-js-sdk/pull/123)
* Implement HTTP timeouts in realtime
[\#121](https://github.com/matrix-org/matrix-js-sdk/pull/121)
Changes in [0.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.1) (2016-03-30)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.0...v0.5.1)
* Only count joined members for the member count in notifications.
[\#119](https://github.com/matrix-org/matrix-js-sdk/pull/119)
* Add maySendEvent to match maySendStateEvent
[\#118](https://github.com/matrix-org/matrix-js-sdk/pull/118)
Changes in [0.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.0) (2016-03-22)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.4.2...v0.5.0)
**BREAKING CHANGES**:
* `opts.pendingEventOrdering`==`end` is no longer supported in the arguments to
`MatrixClient.startClient()`. Instead we provide a `detached` option, which
puts pending events into a completely separate list in the Room, accessible
via `Room.getPendingEvents()`.
[\#111](https://github.com/matrix-org/matrix-js-sdk/pull/111)
Other improvements:
* Log the stack when we get a sync error
[\#109](https://github.com/matrix-org/matrix-js-sdk/pull/109)
* Refactor transmitted-messages code
[\#110](https://github.com/matrix-org/matrix-js-sdk/pull/110)
* Add a method to the js sdk to look up 3pids on the ID server.
[\#113](https://github.com/matrix-org/matrix-js-sdk/pull/113)
* Support for cancelling pending events
[\#112](https://github.com/matrix-org/matrix-js-sdk/pull/112)
* API to stop peeking
[\#114](https://github.com/matrix-org/matrix-js-sdk/pull/114)
* update store user metadata based on membership events rather than presence
[\#116](https://github.com/matrix-org/matrix-js-sdk/pull/116)
* Include a counter in generated transaction IDs
[\#115](https://github.com/matrix-org/matrix-js-sdk/pull/115)
* get/setRoomVisibility API
[\#117](https://github.com/matrix-org/matrix-js-sdk/pull/117)
Changes in [0.4.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.4.2) (2016-03-17)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.4.1...v0.4.2)
* Try again if a pagination request gives us no new messages
[\#98](https://github.com/matrix-org/matrix-js-sdk/pull/98)
* Add a delay before we start polling the connectivity check endpoint
[\#99](https://github.com/matrix-org/matrix-js-sdk/pull/99)
* Clean up a codepath that was only used for crypto messages
[\#101](https://github.com/matrix-org/matrix-js-sdk/pull/101)
* Add maySendStateEvent method, ported from react-sdk (but fixed).
[\#94](https://github.com/matrix-org/matrix-js-sdk/pull/94)
* Add Session.logged_out event
[\#100](https://github.com/matrix-org/matrix-js-sdk/pull/100)
* make presence work when peeking.
[\#103](https://github.com/matrix-org/matrix-js-sdk/pull/103)
* Add RoomState.mayClientSendStateEvent()
[\#104](https://github.com/matrix-org/matrix-js-sdk/pull/104)
* Fix displaynames for member join events
[\#108](https://github.com/matrix-org/matrix-js-sdk/pull/108)
Changes in 0.4.1
================
Improvements:
* Check that `/sync` filters are correct before reusing them, and recreate
them if not (https://github.com/matrix-org/matrix-js-sdk/pull/85).
* Fire a `Room.timelineReset` event when a room's timeline is reset by a gappy
`/sync` (https://github.com/matrix-org/matrix-js-sdk/pull/87,
https://github.com/matrix-org/matrix-js-sdk/pull/93).
* Make `TimelineWindow.load()` faster in the simple case of loading the live
timeline (https://github.com/matrix-org/matrix-js-sdk/pull/88).
* Update room-name calculation code to use the name of the sender of the
invite when invited to a room
(https://github.com/matrix-org/matrix-js-sdk/pull/89).
* Don't reset the timeline when we join a room after peeking into it
(https://github.com/matrix-org/matrix-js-sdk/pull/91).
* Fire `Room.localEchoUpdated` events as local echoes progress through their
transmission process (https://github.com/matrix-org/matrix-js-sdk/pull/95,
https://github.com/matrix-org/matrix-js-sdk/pull/97).
* Avoid getting stuck in a pagination loop when the server sends us only
messages we've already seen
(https://github.com/matrix-org/matrix-js-sdk/pull/96).
New methods:
* Add `MatrixClient.setPushRuleActions` to set the actions for a push
notification rule (https://github.com/matrix-org/matrix-js-sdk/pull/90)
* Add `RoomState.maySendStateEvent` which determines if a given user has
permission to send a state event
(https://github.com/matrix-org/matrix-js-sdk/pull/94)
Changes in 0.4.0
================
**BREAKING CHANGES**:
* `RoomMember.getAvatarUrl()` and `MatrixClient.mxcUrlToHttp()` now return the
empty string when given anything other than an mxc:// URL. This ensures that
clients never inadvertantly reference content directly, leaking information
to third party servers. The `allowDirectLinks` option is provided if the client
wants to allow such links.
* Add a 'bindEmail' option to register()
Improvements:
* Support third party invites
* More appropriate naming for third party invite rooms
* Poll the 'versions' endpoint to re-establish connectivity
* Catch exceptions when syncing
* Room tag support
* Generate implicit read receipts
* Support CAS login
* Guest access support
* Never return non-mxc URLs by default
* Ability to cancel file uploads
* Use the Matrix C/S API v2 with r0 prefix
* Account data support
* Support non-contiguous event timelines
* Support new unread counts
* Local echo for read-receipts
New methods:
* Add method to fetch URLs not on the home or identity server
* Method to get the last receipt for a user
* Method to get all known users
* Method to delete an alias
Changes in 0.3.0
================
* `MatrixClient.getAvatarUrlForMember` has been removed and replaced with
`RoomMember.getAvatarUrl`. Arguments remain the same except the homeserver
URL must now be supplied from `MatrixClient.getHomeserverUrl()`.
```javascript
// before
var url = client.getAvatarUrlForMember(member, width, height, resize, allowDefault)
// after
var url = member.getAvatarUrl(client.getHomeserverUrl(), width, height, resize, allowDefault)
```
* `MatrixClient.getAvatarUrlForRoom` has been removed and replaced with
`Room.getAvatarUrl`. Arguments remain the same except the homeserver
URL must now be supplied from `MatrixClient.getHomeserverUrl()`.
```javascript
// before
var url = client.getAvatarUrlForRoom(room, width, height, resize, allowDefault)
// after
var url = room.getAvatarUrl(client.getHomeserverUrl(), width, height, resize, allowDefault)
```
* `s/Room.getMembersWithMemership/Room.getMembersWithMem`b`ership/g`
New methods:
* Added support for sending receipts via
`MatrixClient.sendReceipt(event, receiptType, callback)` and
`MatrixClient.sendReadReceipt(event, callback)`.
* Added support for receiving receipts via
`Room.getReceiptsForEvent(event)` and `Room.getUsersReadUpTo(event)`. Receipts
can be directly added to a `Room` using `Room.addReceipt(event)` though the
`MatrixClient` does this for you.
* Added support for muting local video and audio via the new methods
`MatrixCall.setMicrophoneMuted()`, `MatrixCall.isMicrophoneMuted(muted)`,
`MatrixCall.isLocalVideoMuted()` and `Matrix.setLocalVideoMuted(muted)`.
* Added **experimental** support for screen-sharing in Chrome via
`MatrixCall.placeScreenSharingCall(remoteVideoElement, localVideoElement)`.
* Added ability to perform server-side searches using
`MatrixClient.searchMessageText(opts)` and `MatrixClient.search(opts)`.
Improvements:
* Improve the performance of initial sync processing from `O(n^2)` to `O(n)`.
* `Room.name` will now take into account `m.room.canonical_alias` events.
* `MatrixClient.startClient` now takes an Object `opts` rather than a Number in
a backwards-compatible way. This `opts` allows syncing configuration options
to be specified including `includeArchivedRooms` and `resolveInvitesToProfiles`.
* `Room` objects which represent room invitations will now have state populated
from `invite_room_state` if it is included in the `m.room.member` event.
* `Room.getAvatarUrl` will now take into account `m.room.avatar` events.
Changes in 0.2.2
================
@@ -22,6 +600,10 @@ New methods:
Changes in 0.2.1
================
**BREAKING CHANGES**
* `MatrixClient.joinRoom` has changed from `(roomIdOrAlias, callback)` to
`(roomIdOrAlias, opts, callback)`.
Bug fixes:
* The `Content-Type` of file uploads is now explicitly set, without relying
on the browser to do it for us.
@@ -33,10 +615,6 @@ Improvements:
* There is now a try/catch block around the `request` function which will
reject/errback appropriately if an exception is thrown synchronously in it.
Breaking changes:
* `MatrixClient.joinRoom` has changed from `(roomIdOrAlias, callback)` to
`(roomIdOrAlias, opts, callback)`.
New methods:
* `MatrixClient.createAlias(alias, roomId)`
* `MatrixClient.getRoomIdForAlias(alias)`
@@ -57,7 +635,7 @@ Modified methods:
Changes in 0.2.0
================
Breaking changes:
**BREAKING CHANGES**:
* `MatrixClient.setPowerLevel` now expects a `MatrixEvent` and not an `Object`
for the `event` parameter.
@@ -88,7 +666,7 @@ New methods:
* `MatrixClient.mxcUrlToHttp(url, w, h, method)`
* `MatrixClient.getAvatarUrlForRoom(room, w, h, method)`
* `MatrixClient.uploadContent(file, callback)`
* `Room.getMembersWithMemership(membership)`
* `Room.getMembersWithMembership(membership)`
* `MatrixScheduler.getQueueForEvent(event)`
* `MatrixScheduler.removeEventFromQueue(event)`
* `$DATA_STORE.setSyncToken(token)`
@@ -123,7 +701,7 @@ Bug fixes:
Changes in 0.1.1
================
Breaking changes:
**BREAKING CHANGES**:
* `Room.calculateRoomName` is now private. Use `Room.recalculate` instead, and
access the calculated name via `Room.name`.
* `new MatrixClient(...)` no longer creates a `MatrixInMemoryStore` if
+4
View File
@@ -0,0 +1,4 @@
Contributing code to matrix-js-sdk
==================================
matrix-js-sdk follows the same pattern as https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst
+37 -31
View File
@@ -10,11 +10,12 @@ Quickstart
In a browser
------------
Copy ``dist/$VERSION/browser-matrix-$VERSION.js`` and add that as a ``<script>`` to
your page. There will be a global variable ``matrixcs`` attached to
``window`` through which you can access the SDK.
Download either the full or minified version from
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
``<script>`` to your page. There will be a global variable ``matrixcs``
attached to ``window`` through which you can access the SDK.
Please check [the working browser example](examples/browser) for more information.
Please check [the working browser example](examples/browser) for more information.
In Node.js
----------
@@ -78,7 +79,7 @@ are updated.
client.on("event", function(event) {
console.log(event.getType());
});
// Listen for typing changes
client.on("RoomMember.typing", function(event, member) {
if (member.typing) {
@@ -88,38 +89,43 @@ are updated.
console.log(member.name + " stopped typing.");
}
});
// start the client to setup the connection to the server
client.startClient();
```
### Promises or Callbacks
### Promises and Callbacks
The SDK supports *both* callbacks and Promises (Q). The convention
you'll see used is:
Most of the methods in the SDK are asynchronous: they do not directly return a
result, but instead return a [Promise](http://documentup.com/kriskowal/q/)
which will be fulfilled in the future.
The typical usage is something like:
```javascript
var promise = matrixClient.someMethod(arg1, arg2, callback);
```
The ``callback`` parameter is optional, so you could do:
```javascript
matrixClient.someMethod(arg1, arg2).then(function(err, result) {
matrixClient.someMethod(arg1, arg2).done(function(result) {
...
});
```
Alternatively, you could do:
Alternatively, if you have a Node.js-style ``callback(err, result)`` function,
you can pass the result of the promise into it with something like:
```javascript
matrixClient.someMethod(arg1, arg2, function(result) {
...
});
matrixClient.someMethod(arg1, arg2).nodeify(callback);
```
Methods which support this will be clearly marked as returning
``Promises``.
The main thing to note is that it is an error to discard the result of a
promise-returning function, as that will cause exceptions to go unobserved. If
you have nothing better to do with the result, just call ``.done()`` on it. See
http://documentup.com/kriskowal/q/#the-end for more information.
Methods which return a promise show this in their documentation.
Many methods in the SDK support *both* Node.js-style callbacks *and* Promises,
via an optional ``callback`` argument. The callback support is now deprecated:
new methods do not include a ``callback`` argument, and in the future it may be
removed from existing methods.
Examples
--------
@@ -147,10 +153,10 @@ core functionality of the SDK. These examples assume the SDK is setup like this:
});
}
});
matrixClient.startClient();
```
### Print out messages for all rooms
```javascript
@@ -166,7 +172,7 @@ core functionality of the SDK. These examples assume the SDK is setup like this:
"(%s) %s :: %s", room.name, event.getSender(), event.getContent().body
);
});
matrixClient.startClient();
```
@@ -198,10 +204,10 @@ Output:
);
}
});
matrixClient.startClient();
```
Output:
```
My Room
@@ -211,7 +217,7 @@ Output:
(join) Bob
(invite) @charlie:localhost
```
API Reference
=============
@@ -226,7 +232,7 @@ host the API reference from the source files like this:
$ cd .jsdoc
$ python -m SimpleHTTPServer 8005
```
Then visit ``http://localhost:8005`` to see the API docs.
Contributing
@@ -256,7 +262,7 @@ To run tests (Jasmine)::
```
$ npm test
```
To run linting:
```
$ npm run lint
+14
View File
@@ -0,0 +1,14 @@
There is a script `release.sh` which does the following, but if you need to do
a release manually, here are the steps:
- `git checkout -b release-v0.x.x`
- Update `CHANGELOG.md`
- `npm version 0.x.x`
- Merge `release-v0.x.x` onto `master`.
- Push `master`.
- Push the tag: `git push --tags`
- `npm publish`
- Generate documentation: `npm run gendoc` (this outputs HTML to `.jsdoc`)
- Copy the documentation from `.jsdoc` to the `gh-pages` branch and update `index.html`
- Merge `master` onto `develop`.
- Push `develop`.
-5826
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-6490
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-9900
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-10023
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
Vendored
-1
View File
@@ -1 +0,0 @@
Release builds and development builds will reside here.
+1 -1
View File
@@ -1 +1 @@
../../../dist/browser-matrix-dev.js
../../../dist/browser-matrix.js
+9 -5
View File
@@ -135,11 +135,15 @@ rl.on('line', function(line) {
// ==== END User input
// show the room list after syncing.
matrixClient.on("syncComplete", function() {
setRoomList();
printRoomList();
printHelp();
rl.prompt();
matrixClient.on("sync", function(state, prevState, data) {
switch (state) {
case "PREPARED":
setRoomList();
printRoomList();
printHelp();
rl.prompt();
break;
}
});
matrixClient.on("Room", function() {
+10 -2
View File
@@ -44,7 +44,15 @@ window.onload = function() {
disableButtons(true, true, true);
};
client.on("syncComplete", function () {
matrixClient.on("sync", function(state, prevState, data) {
switch (state) {
case "PREPARED":
syncComplete();
break;
}
});
function syncComplete() {
document.getElementById("result").innerHTML = "<p>Ready for calls.</p>";
disableButtons(false, true, true);
@@ -85,5 +93,5 @@ client.on("syncComplete", function () {
call = c;
addListeners(call);
});
});
}
client.startClient();
+1 -1
View File
@@ -1 +1 @@
../../../dist/browser-matrix-dev.js
../../../dist/browser-matrix.js
+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
+3
View File
@@ -1,3 +1,6 @@
var matrixcs = require("./lib/matrix");
matrixcs.request(require("request"));
module.exports = matrixcs;
var utils = require("./lib/utils");
utils.runPolyfills();
Executable
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash -l
export NVM_DIR="/home/jenkins/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 0.10
npm install
RC=0
function fail {
echo $@ >&2
RC=1
}
npm test || fail "npm test finished with return code $?"
jshint --reporter=checkstyle -c .jshint lib spec > jshint.xml ||
fail "jshint finished with return code $?"
gjslint --unix_mode --disable 0131,0211,0200,0222,0212 \
--max_line_length 90 \
-r lib/ -r spec/ > gjslint.log ||
fail "gjslint finished with return code $?"
# delete the old tarball, if it exists
rm -f matrix-js-sdk-*.tgz
npm pack ||
fail "npm pack finished with return code $?"
npm run gendoc || fail "JSDoc failed with code $?"
exit $RC
+1158
View File
File diff suppressed because it is too large Load Diff
+1916 -1386
View File
File diff suppressed because it is too large Load Diff
+105
View File
@@ -0,0 +1,105 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module content-repo
*/
var utils = require("./utils");
/** Content Repo utility functions */
module.exports = {
/**
* Get the HTTP URL for an MXC URI.
* @param {string} baseUrl The base homeserver url which has a content repo.
* @param {string} mxc The mxc:// URI.
* @param {Number} width The desired width of the thumbnail.
* @param {Number} height The desired height of the thumbnail.
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @param {Boolean} allowDirectLinks If true, return any non-mxc URLs
* directly. Fetching such URLs will leak information about the user to
* anyone they share a room with. If false, will return the emptry string
* for such URLs.
* @return {string} The complete URL to the content.
*/
getHttpUriForMxc: function(baseUrl, mxc, width, height,
resizeMethod, allowDirectLinks) {
if (typeof mxc !== "string" || !mxc) {
return '';
}
if (mxc.indexOf("mxc://") !== 0) {
if (allowDirectLinks) {
return mxc;
} else {
return '';
}
}
var serverAndMediaId = mxc.slice(6); // strips mxc://
var prefix = "/_matrix/media/v1/download/";
var params = {};
if (width) {
params.width = width;
}
if (height) {
params.height = height;
}
if (resizeMethod) {
params.method = resizeMethod;
}
if (utils.keys(params).length > 0) {
// these are thumbnailing params so they probably want the
// thumbnailing API...
prefix = "/_matrix/media/v1/thumbnail/";
}
var fragmentOffset = serverAndMediaId.indexOf("#"),
fragment = "";
if (fragmentOffset >= 0) {
fragment = serverAndMediaId.substr(fragmentOffset);
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
}
return baseUrl + prefix + serverAndMediaId +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params))) + fragment;
},
/**
* Get an identicon URL from an arbitrary string.
* @param {string} baseUrl The base homeserver url which has a content repo.
* @param {string} identiconString The string to create an identicon for.
* @param {Number} width The desired width of the image in pixels. Default: 96.
* @param {Number} height The desired height of the image in pixels. Default: 96.
* @return {string} The complete URL to the identicon.
*/
getIdenticonUri: function(baseUrl, identiconString, width, height) {
if (!identiconString) {
return null;
}
if (!width) { width = 96; }
if (!height) { height = 96; }
var params = {
width: width,
height: height
};
var path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
$ident: identiconString
});
return baseUrl + path +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params)));
}
};
+767
View File
@@ -0,0 +1,767 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* olm.js wrapper
*
* @module crypto/OlmDevice
*/
var Olm = require("olm");
var utils = require("../utils");
// The maximum size of an event is 65K, and we base64 the content, so this is a
// reasonable approximation to the biggest plaintext we can encrypt.
var MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4;
function checkPayloadLength(payloadString) {
if (payloadString === undefined) {
throw new Error("payloadString undefined");
}
if (payloadString.length > MAX_PLAINTEXT_LENGTH) {
// might as well fail early here rather than letting the olm library throw
// a cryptic memory allocation error.
//
// Note that even if we manage to do the encryption, the message send may fail,
// because by the time we've wrapped the ciphertext in the event object, it may
// exceed 65K. But at least we won't just fail with "abort()" in that case.
throw new Error("Message too long (" + payloadString.length + " bytes). " +
"The maximum for an encrypted message is " +
MAX_PLAINTEXT_LENGTH + " bytes.");
}
}
/**
* Manages the olm cryptography functions. Each OlmDevice has a single
* OlmAccount and a number of OlmSessions.
*
* Accounts and sessions are kept pickled in a sessionStore.
*
* @constructor
* @alias module:crypto/OlmDevice
*
* @param {Object} sessionStore A store to be used for data in end-to-end
* crypto
*
* @property {string} deviceCurve25519Key Curve25519 key for the account
* @property {string} deviceEd25519Key Ed25519 key for the account
*/
function OlmDevice(sessionStore) {
this._sessionStore = sessionStore;
this._pickleKey = "DEFAULT_KEY";
var e2eKeys;
var account = new Olm.Account();
try {
_initialise_account(this._sessionStore, this._pickleKey, account);
e2eKeys = JSON.parse(account.identity_keys());
} finally {
account.free();
}
this.deviceCurve25519Key = e2eKeys.curve25519;
this.deviceEd25519Key = e2eKeys.ed25519;
// we don't bother stashing outboundgroupsessions in the sessionstore -
// instead we keep them here.
this._outboundGroupSessionStore = {};
// Store a set of decrypted message indexes for each group session.
// This partially mitigates a replay attack where a MITM resends a group
// message into the room.
//
// TODO: If we ever remove an event from memory we will also need to remove
// it from this map. Otherwise if we download the event from the server we
// will think that it is a duplicate.
//
// Keys are strings of form "<senderKey>|<session_id>|<message_index>"
// Values are true.
this._inboundGroupSessionMessageIndexes = {};
}
function _initialise_account(sessionStore, pickleKey, account) {
var e2eAccount = sessionStore.getEndToEndAccount();
if (e2eAccount !== null) {
account.unpickle(pickleKey, e2eAccount);
return;
}
account.create();
var pickled = account.pickle(pickleKey);
sessionStore.storeEndToEndAccount(pickled);
}
/**
* @return {array} The version of Olm.
*/
OlmDevice.getOlmVersion = function() {
return Olm.get_library_version();
};
/**
* extract our OlmAccount from the session store and call the given function
*
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getAccount = function(func) {
var account = new Olm.Account();
try {
var pickledAccount = this._sessionStore.getEndToEndAccount();
account.unpickle(this._pickleKey, pickledAccount);
return func(account);
} finally {
account.free();
}
};
/**
* store our OlmAccount in the session store
*
* @param {OlmAccount} account
* @private
*/
OlmDevice.prototype._saveAccount = function(account) {
var pickledAccount = account.pickle(this._pickleKey);
this._sessionStore.storeEndToEndAccount(pickledAccount);
};
/**
* extract an OlmSession from the session store and call the given function
*
* @param {string} deviceKey
* @param {string} sessionId
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getSession = function(deviceKey, sessionId, func) {
var sessions = this._sessionStore.getEndToEndSessions(deviceKey);
var pickledSession = sessions[sessionId];
var session = new Olm.Session();
try {
session.unpickle(this._pickleKey, pickledSession);
return func(session);
} finally {
session.free();
}
};
/**
* store our OlmSession in the session store
*
* @param {string} deviceKey
* @param {OlmSession} session
* @private
*/
OlmDevice.prototype._saveSession = function(deviceKey, session) {
var pickledSession = session.pickle(this._pickleKey);
this._sessionStore.storeEndToEndSession(
deviceKey, session.session_id(), pickledSession
);
};
/**
* get an OlmUtility and call the given function
*
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getUtility = function(func) {
var utility = new Olm.Utility();
try {
return func(utility);
} finally {
utility.free();
}
};
/**
* Signs a message with the ed25519 key for this account.
*
* @param {string} message message to be signed
* @return {string} base64-encoded signature
*/
OlmDevice.prototype.sign = function(message) {
return this._getAccount(function(account) {
return account.sign(message);
});
};
/**
* Get the current (unused, unpublished) one-time keys for this account.
*
* @return {object} one time keys; an object with the single property
* <tt>curve25519</tt>, which is itself an object mapping key id to Curve25519
* key.
*/
OlmDevice.prototype.getOneTimeKeys = function() {
return this._getAccount(function(account) {
return JSON.parse(account.one_time_keys());
});
};
/**
* Get the maximum number of one-time keys we can store.
*
* @return {number} number of keys
*/
OlmDevice.prototype.maxNumberOfOneTimeKeys = function() {
return this._getAccount(function(account) {
return account.max_number_of_one_time_keys();
});
};
/**
* Marks all of the one-time keys as published.
*/
OlmDevice.prototype.markKeysAsPublished = function() {
var self = this;
this._getAccount(function(account) {
account.mark_keys_as_published();
self._saveAccount(account);
});
};
/**
* Generate some new one-time keys
*
* @param {number} numKeys number of keys to generate
*/
OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
var self = this;
this._getAccount(function(account) {
account.generate_one_time_keys(numKeys);
self._saveAccount(account);
});
};
/**
* Generate a new outbound session
*
* The new session will be stored in the sessionStore.
*
* @param {string} theirIdentityKey remote user's Curve25519 identity key
* @param {string} theirOneTimeKey remote user's one-time Curve25519 key
* @return {string} sessionId for the outbound session.
*/
OlmDevice.prototype.createOutboundSession = function(
theirIdentityKey, theirOneTimeKey
) {
var self = this;
return this._getAccount(function(account) {
var session = new Olm.Session();
try {
session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
self._saveSession(theirIdentityKey, session);
return session.session_id();
} finally {
session.free();
}
});
};
/**
* Generate a new inbound session, given an incoming message
*
* @param {string} theirDeviceIdentityKey remote user's Curve25519 identity key
* @param {number} message_type message_type field from the received message (must be 0)
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {{payload: string, session_id: string}} decrypted payload, and
* session id of new session
*
* @raises {Error} if the received message was not valid (for instance, it
* didn't use a valid one-time key).
*/
OlmDevice.prototype.createInboundSession = function(
theirDeviceIdentityKey, message_type, ciphertext
) {
if (message_type !== 0) {
throw new Error("Need message_type == 0 to create inbound session");
}
var self = this;
return this._getAccount(function(account) {
var session = new Olm.Session();
try {
session.create_inbound_from(account, theirDeviceIdentityKey, ciphertext);
account.remove_one_time_keys(session);
self._saveAccount(account);
var payloadString = session.decrypt(message_type, ciphertext);
self._saveSession(theirDeviceIdentityKey, session);
return {
payload: payloadString,
session_id: session.session_id(),
};
} finally {
session.free();
}
});
};
/**
* Get a list of known session IDs for the given device
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @return {string[]} a list of known session ids for the device
*/
OlmDevice.prototype.getSessionIdsForDevice = function(theirDeviceIdentityKey) {
var sessions = this._sessionStore.getEndToEndSessions(
theirDeviceIdentityKey
);
return utils.keys(sessions);
};
/**
* Get the right olm session id for encrypting messages to the given identity key
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @return {string?} session id, or null if no established session
*/
OlmDevice.prototype.getSessionIdForDevice = function(theirDeviceIdentityKey) {
var sessionIds = this.getSessionIdsForDevice(theirDeviceIdentityKey);
if (sessionIds.length === 0) {
return null;
}
// Use the session with the lowest ID.
sessionIds.sort();
return sessionIds[0];
};
/**
* Get information on the active Olm sessions for a device.
* <p>
* Returns an array, with an entry for each active session. The first entry in
* the result will be the one used for outgoing messages. Each entry contains
* the keys 'hasReceivedMessage' (true if the session has received an incoming
* message and is therefore past the pre-key stage), and 'sessionId'.
*
* @param {string} deviceIdentityKey Curve25519 identity key for the device
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
*/
OlmDevice.prototype.getSessionInfoForDevice = function(deviceIdentityKey) {
var sessionIds = this.getSessionIdsForDevice(deviceIdentityKey);
sessionIds.sort();
var info = [];
function getSessionInfo(session) {
return {
hasReceivedMessage: session.has_received_message()
};
}
for (var i = 0; i < sessionIds.length; i++) {
var sessionId = sessionIds[i];
var res = this._getSession(deviceIdentityKey, sessionId, getSessionInfo);
res.sessionId = sessionId;
info.push(res);
}
return info;
};
/**
* Encrypt an outgoing message using an existing session
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {string} payloadString payload to be encrypted and sent
*
* @return {string} ciphertext
*/
OlmDevice.prototype.encryptMessage = function(
theirDeviceIdentityKey, sessionId, payloadString
) {
var self = this;
checkPayloadLength(payloadString);
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
var res = session.encrypt(payloadString);
self._saveSession(theirDeviceIdentityKey, session);
return res;
});
};
/**
* Decrypt an incoming message using an existing session
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {number} message_type message_type field from the received message
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {string} decrypted payload.
*/
OlmDevice.prototype.decryptMessage = function(
theirDeviceIdentityKey, sessionId, message_type, ciphertext
) {
var self = this;
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
var payloadString = session.decrypt(message_type, ciphertext);
self._saveSession(theirDeviceIdentityKey, session);
return payloadString;
});
};
/**
* Determine if an incoming messages is a prekey message matching an existing session
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {number} message_type message_type field from the received message
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {boolean} true if the received message is a prekey message which matches
* the given session.
*/
OlmDevice.prototype.matchesSession = function(
theirDeviceIdentityKey, sessionId, message_type, ciphertext
) {
if (message_type !== 0) {
return false;
}
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
return session.matches_inbound(ciphertext);
});
};
// Outbound group session
// ======================
/**
* store an OutboundGroupSession in _outboundGroupSessionStore
*
* @param {Olm.OutboundGroupSession} session
* @private
*/
OlmDevice.prototype._saveOutboundGroupSession = function(session) {
var pickledSession = session.pickle(this._pickleKey);
this._outboundGroupSessionStore[session.session_id()] = pickledSession;
};
/**
* extract an OutboundGroupSession from _outboundGroupSessionStore and call the
* given function
*
* @param {string} sessionId
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
var pickled = this._outboundGroupSessionStore[sessionId];
if (pickled === null) {
throw new Error("Unknown outbound group session " + sessionId);
}
var session = new Olm.OutboundGroupSession();
try {
session.unpickle(this._pickleKey, pickled);
return func(session);
} finally {
session.free();
}
};
/**
* Generate a new outbound group session
*
* @return {string} sessionId for the outbound session.
*/
OlmDevice.prototype.createOutboundGroupSession = function() {
var session = new Olm.OutboundGroupSession();
try {
session.create();
this._saveOutboundGroupSession(session);
return session.session_id();
} finally {
session.free();
}
};
/**
* Encrypt an outgoing message with an outbound group session
*
* @param {string} sessionId the id of the outboundgroupsession
* @param {string} payloadString payload to be encrypted and sent
*
* @return {string} ciphertext
*/
OlmDevice.prototype.encryptGroupMessage = function(sessionId, payloadString) {
var self = this;
checkPayloadLength(payloadString);
return this._getOutboundGroupSession(sessionId, function(session) {
var res = session.encrypt(payloadString);
self._saveOutboundGroupSession(session);
return res;
});
};
/**
* Get the session keys for an outbound group session
*
* @param {string} sessionId the id of the outbound group session
*
* @return {{chain_index: number, key: string}} current chain index, and
* base64-encoded secret key.
*/
OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
return this._getOutboundGroupSession(sessionId, function(session) {
return {
chain_index: session.message_index(),
key: session.session_key(),
};
});
};
// Inbound group session
// =====================
/**
* store an InboundGroupSession in the session store
*
* @param {string} roomId
* @param {string} senderCurve25519Key
* @param {string} sessionId
* @param {Olm.InboundGroupSession} session
* @param {object} keysClaimed Other keys the sender claims.
* @private
*/
OlmDevice.prototype._saveInboundGroupSession = function(
roomId, senderCurve25519Key, sessionId, session, keysClaimed
) {
var r = {
room_id: roomId,
session: session.pickle(this._pickleKey),
keysClaimed: keysClaimed,
};
this._sessionStore.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, JSON.stringify(r)
);
};
/**
* extract an InboundGroupSession from the session store and call the given function
*
* @param {string} roomId
* @param {string} senderKey
* @param {string} sessionId
* @param {function(Olm.InboundGroupSession, Object<string, string>): T} func
* function to call. Second argument is the map of keys claimed by the session.
*
* @return {null} the sessionId is unknown
*
* @return {T} result of func
*
* @private
* @template {T}
*/
OlmDevice.prototype._getInboundGroupSession = function(
roomId, senderKey, sessionId, func
) {
var r = this._sessionStore.getEndToEndInboundGroupSession(
senderKey, sessionId
);
if (r === null) {
return null;
}
r = JSON.parse(r);
// check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room.
if (roomId !== r.room_id) {
throw new Error(
"Mismatched room_id for inbound group session (expected " + r.room_id +
", was " + roomId + ")"
);
}
var session = new Olm.InboundGroupSession();
try {
session.unpickle(this._pickleKey, r.session);
return func(session, r.keysClaimed || {});
} finally {
session.free();
}
};
/**
* Add an inbound group session to the session store
*
* @param {string} roomId room in which this session will be used
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {string} sessionKey base64-encoded secret key
* @param {Object<string, string>} keysClaimed Other keys the sender claims.
*/
OlmDevice.prototype.addInboundGroupSession = function(
roomId, senderKey, sessionId, sessionKey, keysClaimed
) {
var self = this;
/* if we already have this session, consider updating it */
function updateSession(session) {
console.log("Update for megolm session " + senderKey + "/" + sessionId);
// for now we just ignore updates. TODO: implement something here
return true;
}
var r = this._getInboundGroupSession(
roomId, senderKey, sessionId, updateSession
);
if (r !== null) {
return;
}
// new session.
var session = new Olm.InboundGroupSession();
try {
session.create(sessionKey);
if (sessionId != session.session_id()) {
throw new Error(
"Mismatched group session ID from senderKey: " + senderKey
);
}
self._saveInboundGroupSession(
roomId, senderKey, sessionId, session, keysClaimed
);
} finally {
session.free();
}
};
/**
* Decrypt a received message with an inbound group session
*
* @param {string} roomId room in which the message was received
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {string} body base64-encoded body of the encrypted message
*
* @return {null} the sessionId is unknown
*
* @return {{result: string, keysProved: Object<string, string>, keysClaimed:
* Object<string, string>}} result
*/
OlmDevice.prototype.decryptGroupMessage = function(
roomId, senderKey, sessionId, body
) {
var self = this;
function decrypt(session, keysClaimed) {
var res = session.decrypt(body);
var plaintext = res.plaintext;
if (plaintext === undefined) {
// Compatibility for older olm versions.
plaintext = res;
} else {
// Check if we have seen this message index before to detect replay attacks.
var messageIndexKey = senderKey + "|" + sessionId + "|" + res.message_index;
if (messageIndexKey in self._inboundGroupSessionMessageIndexes) {
throw new Error(
"Duplicate message index, possible replay attack: " +
messageIndexKey
);
}
self._inboundGroupSessionMessageIndexes[messageIndexKey] = true;
}
// the sender must have had the senderKey to persuade us to save the
// session.
var keysProved = {curve25519: senderKey};
self._saveInboundGroupSession(
roomId, senderKey, sessionId, session, keysClaimed
);
return {
result: plaintext,
keysClaimed: keysClaimed,
keysProved: keysProved,
};
}
return this._getInboundGroupSession(
roomId, senderKey, sessionId, decrypt
);
};
// Utilities
// =========
/**
* Verify an ed25519 signature.
*
* @param {string} key ed25519 key
* @param {string} message message which was signed
* @param {string} signature base64-encoded signature to be checked
*
* @raises {Error} if there is a problem with the verification. If the key was
* too small then the message will be "OLM.INVALID_BASE64". If the signature
* was invalid then the message will be "OLM.BAD_MESSAGE_MAC".
*/
OlmDevice.prototype.verifySignature = function(
key, message, signature
) {
this._getUtility(function(util) {
util.ed25519_verify(key, message, signature);
});
};
/** */
module.exports = OlmDevice;
+167
View File
@@ -0,0 +1,167 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* Internal module. Defines the base classes of the encryption implementations
*
* @module crypto/algorithms/base
*/
var utils = require("../../utils");
/**
* map of registered encryption algorithm classes. A map from string to {@link
* module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} class
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
*/
module.exports.ENCRYPTION_CLASSES = {};
/**
* map of registered encryption algorithm classes. Map from string to {@link
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} class
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
*/
module.exports.DECRYPTION_CLASSES = {};
/**
* base type for encryption implementations
*
* @constructor
* @alias module:crypto/algorithms/base.EncryptionAlgorithm
*
* @param {object} params parameters
* @param {string} params.userId The UserID for the local user
* @param {string} params.deviceId The identifier for this device.
* @param {module:crypto} params.crypto crypto core
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
* @param {string} params.roomId The ID of the room we will be sending to
* @param {object} params.config The body of the m.room.encryption event
*/
var EncryptionAlgorithm = function(params) {
this._userId = params.userId;
this._deviceId = params.deviceId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._baseApis = params.baseApis;
this._roomId = params.roomId;
};
/** */
module.exports.EncryptionAlgorithm = EncryptionAlgorithm;
/**
* Encrypt a message event
*
* @method module:crypto/algorithms/base.EncryptionAlgorithm#encryptMessage
* @abstract
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
/**
* Called when the membership of a member of the room changes.
*
* @param {module:models/event.MatrixEvent} event event causing the change
* @param {module:models/room-member} member user whose membership changed
* @param {string=} oldMembership previous membership
*/
EncryptionAlgorithm.prototype.onRoomMembership = function(
event, member, oldMembership
) {};
/**
* base type for decryption implementations
*
* @constructor
* @alias module:crypto/algorithms/base.DecryptionAlgorithm
*
* @param {object} params parameters
* @param {string} params.userId The UserID for the local user
* @param {module:crypto} params.crypto crypto core
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
* @param {string=} params.roomId The ID of the room we will be receiving
* from. Null for to-device events.
*/
var DecryptionAlgorithm = function(params) {
this._userId = params.userId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._roomId = params.roomId;
};
/** */
module.exports.DecryptionAlgorithm = DecryptionAlgorithm;
/**
* Decrypt an event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent
* @abstract
*
* @param {object} event raw event
*
* @return {null} if the event referred to an unknown megolm session
* @return {module:crypto.DecryptionResult} decryption result
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
/**
* Handle a key event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent
*
* @param {module:models/event.MatrixEvent} event key event
*/
DecryptionAlgorithm.prototype.onRoomKeyEvent = function(params) {
// ignore by default
};
/**
* Exception thrown when decryption fails
*
* @constructor
* @param {string} msg message describing the problem
* @extends Error
*/
module.exports.DecryptionError = function(msg) {
this.message = msg;
};
utils.inherits(module.exports.DecryptionError, Error);
/**
* Registers an encryption/decryption class for a particular algorithm
*
* @param {string} algorithm algorithm tag to register for
*
* @param {class} encryptor {@link
* module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm}
* implementation
*
* @param {class} decryptor {@link
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm}
* implementation
*/
module.exports.registerAlgorithm = function(algorithm, encryptor, decryptor) {
module.exports.ENCRYPTION_CLASSES[algorithm] = encryptor;
module.exports.DECRYPTION_CLASSES[algorithm] = decryptor;
};
+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;
+586
View File
@@ -0,0 +1,586 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* Defines m.olm encryption/decryption
*
* @module crypto/algorithms/megolm
*/
var q = require("q");
var utils = require("../../utils");
var olmlib = require("../olmlib");
var base = require("./base");
/**
* @private
* @constructor
*
* @param {string} sessionId
*
* @property {string} sessionId
* @property {Number} useCount number of times this session has been used
* @property {Number} creationTime when the session was created (ms since the epoch)
*
* @property {object} sharedWithDevices
* devices with which we have shared the session key
* userId -> {deviceId -> msgindex}
*/
function OutboundSessionInfo(sessionId) {
this.sessionId = sessionId;
this.useCount = 0;
this.creationTime = new Date().getTime();
this.sharedWithDevices = {};
}
/**
* Check if it's time to rotate the session
*
* @param {Number} rotationPeriodMsgs
* @param {Number} rotationPeriodMs
* @return {Boolean}
*/
OutboundSessionInfo.prototype.needsRotation = function(
rotationPeriodMsgs, rotationPeriodMs
) {
var sessionLifetime = new Date().getTime() - this.creationTime;
if (this.useCount >= rotationPeriodMsgs ||
sessionLifetime >= rotationPeriodMs
) {
console.log(
"Rotating megolm session after " + this.useCount +
" messages, " + sessionLifetime + "ms"
);
return true;
}
return false;
};
/**
* Determine if this session has been shared with devices which it shouldn't
* have been.
*
* @param {Object} devicesInRoom userId -> {deviceId -> object}
* devices we should shared the session with.
*
* @return {Boolean} true if we have shared the session with devices which aren't
* in devicesInRoom.
*/
OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
devicesInRoom
) {
for (var userId in this.sharedWithDevices) {
if (!this.sharedWithDevices.hasOwnProperty(userId)) { continue; }
if (!devicesInRoom.hasOwnProperty(userId)) {
console.log("Starting new session because we shared with " + userId);
return true;
}
for (var deviceId in this.sharedWithDevices[userId]) {
if (!this.sharedWithDevices[userId].hasOwnProperty(deviceId)) {
continue;
}
if (!devicesInRoom[userId].hasOwnProperty(deviceId)) {
console.log(
"Starting new session because we shared with " +
userId + ":" + deviceId
);
return true;
}
}
}
};
/**
* Megolm encryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.EncryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.EncryptionAlgorithm}
*/
function MegolmEncryption(params) {
base.EncryptionAlgorithm.call(this, params);
// the most recent attempt to set up a session. This is used to serialise
// the session setups, so that we have a race-free view of which session we
// are using, and which devices we have shared the keys with. It resolves
// with an OutboundSessionInfo (or undefined, for the first message in the
// room).
this._setupPromise = q();
// default rotation periods
this._sessionRotationPeriodMsgs = 100;
this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
if (params.config.rotation_period_ms !== undefined) {
this._sessionRotationPeriodMs = params.config.rotation_period_ms;
}
if (params.config.rotation_period_msgs !== undefined) {
this._sessionRotationPeriodMsgs = params.config.rotation_period_msgs;
}
}
utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
/**
* @private
*
* @param {module:models/room} room
*
* @return {module:client.Promise} Promise which resolves to the
* OutboundSessionInfo when setup is complete.
*/
MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
var self = this;
var session;
// takes the previous OutboundSessionInfo, and considers whether to create
// a new one. Also shares the key with any (new) devices in the room.
// Updates `session` to hold the final OutboundSessionInfo.
//
// returns a promise which resolves once the keyshare is successful.
function prepareSession(oldSession) {
session = oldSession;
// need to make a brand new session?
if (session && session.needsRotation(self._sessionRotationPeriodMsgs,
self._sessionRotationPeriodMs)
) {
console.log("Starting new megolm session because we need to rotate.");
session = null;
}
// determine if we have shared with anyone we shouldn't have
if (session && session.sharedWithTooManyDevices(devicesInRoom)) {
session = null;
}
if (!session) {
session = self._prepareNewSession();
}
// now check if we need to share with any devices
var shareMap = {};
for (var userId in devicesInRoom) {
if (!devicesInRoom.hasOwnProperty(userId)) {
continue;
}
var userDevices = devicesInRoom[userId];
for (var deviceId in userDevices) {
if (!userDevices.hasOwnProperty(deviceId)) {
continue;
}
var deviceInfo = userDevices[deviceId];
var key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
continue;
}
if (
!session.sharedWithDevices[userId] ||
session.sharedWithDevices[userId][deviceId] === undefined
) {
shareMap[userId] = shareMap[userId] || [];
shareMap[userId].push(deviceInfo);
}
}
}
return self._shareKeyWithDevices(
session, shareMap
);
}
// helper which returns the session prepared by prepareSession
function returnSession() { return session; }
// first wait for the previous share to complete
var prom = this._setupPromise.then(prepareSession);
// _setupPromise resolves to `session` whether or not the share succeeds
this._setupPromise = prom.then(returnSession, returnSession);
// but we return a promise which only resolves if the share was successful.
return prom.then(returnSession);
};
/**
* @private
*
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*/
MegolmEncryption.prototype._prepareNewSession = function() {
var session_id = this._olmDevice.createOutboundGroupSession();
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
this._olmDevice.addInboundGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, session_id,
key.key, {ed25519: this._olmDevice.deviceEd25519Key}
);
return new OutboundSessionInfo(session_id);
};
/**
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @return {module:client.Promise} Promise which resolves once the key sharing
* message has been sent.
*/
MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUser) {
var self = this;
var key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
var payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: session.sessionId,
session_key: key.key,
chain_index: key.chain_index,
}
};
var contentMap = {};
return olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser
).then(function(devicemap) {
var haveTargets = false;
for (var userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
var devicesToShareWith = devicesByUser[userId];
var sessionResults = devicemap[userId];
for (var i = 0; i < devicesToShareWith.length; i++) {
var deviceInfo = devicesToShareWith[i];
var deviceId = deviceInfo.deviceId;
var sessionResult = sessionResults[deviceId];
if (!sessionResult.sessionId) {
// no session with this device, probably because there
// were no one-time keys.
//
// we could send them a to_device message anyway, as a
// signal that they have missed out on the key sharing
// message because of the lack of keys, but there's not
// much point in that really; it will mostly serve to clog
// up to_device inboxes.
//
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
continue;
}
console.log(
"sharing keys with device " + userId + ":" + deviceId
);
var encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
self._userId,
self._deviceId,
self._olmDevice,
userId,
deviceInfo,
payload
);
if (!contentMap[userId]) {
contentMap[userId] = {};
}
contentMap[userId][deviceId] = encryptedContent;
haveTargets = true;
}
}
if (!haveTargets) {
return q();
}
// TODO: retries
return self._baseApis.sendToDevice("m.room.encrypted", contentMap);
}).then(function() {
// Add the devices we have shared with to session.sharedWithDevices.
//
// we deliberately iterate over devicesByUser (ie, the devices we
// attempted to share with) rather than the contentMap (those we did
// share with), because we don't want to try to claim a one-time-key
// for dead devices on every message.
for (var userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
if (!session.sharedWithDevices[userId]) {
session.sharedWithDevices[userId] = {};
}
var devicesToShareWith = devicesByUser[userId];
for (var i = 0; i < devicesToShareWith.length; i++) {
var deviceInfo = devicesToShareWith[i];
session.sharedWithDevices[userId][deviceInfo.deviceId] =
key.chain_index;
}
}
});
};
/**
* @inheritdoc
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
var self = this;
return this._getDevicesInRoom(room).then(function(devicesInRoom) {
return self._ensureOutboundSession(devicesInRoom);
}).then(function(session) {
var payloadJson = {
room_id: self._roomId,
type: eventType,
content: content
};
var ciphertext = self._olmDevice.encryptGroupMessage(
session.sessionId, JSON.stringify(payloadJson)
);
var encryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext,
session_id: session.sessionId,
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
device_id: self._deviceId,
};
session.useCount++;
return encryptedContent;
});
};
/**
* Get the list of unblocked devices for all users in the room
*
* @param {module:models/room} room
*
* @return {module:client.Promise} Promise which resolves to a map
* from userId to deviceId to deviceInfo
*/
MegolmEncryption.prototype._getDevicesInRoom = function(room) {
// XXX what about rooms where invitees can see the content?
var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
// We are happy to use a cached version here: we assume that if we already
// have a list of the user's devices, then we already share an e2e room
// with them, which means that they will have announced any new devices via
// an m.new_device.
return this._crypto.downloadKeys(roomMembers, false).then(function(devices) {
// remove any blocked devices
for (var userId in devices) {
if (!devices.hasOwnProperty(userId)) {
continue;
}
var userDevices = devices[userId];
for (var deviceId in userDevices) {
if (!userDevices.hasOwnProperty(deviceId)) {
continue;
}
if (userDevices[deviceId].isBlocked()) {
delete userDevices[deviceId];
}
}
}
return devices;
});
};
/**
* Megolm decryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.DecryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.DecryptionAlgorithm}
*/
function MegolmDecryption(params) {
base.DecryptionAlgorithm.call(this, params);
// events which we couldn't decrypt due to unknown sessions / indexes: map from
// senderKey|sessionId to list of MatrixEvents
this._pendingEvents = {};
}
utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
/**
* @inheritdoc
*
* @param {MatrixEvent} event
*
* @return {null} The event referred to an unknown megolm session
* @return {module:crypto.DecryptionResult} decryption result
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
MegolmDecryption.prototype.decryptEvent = function(event) {
var content = event.getWireContent();
if (!content.sender_key || !content.session_id ||
!content.ciphertext
) {
throw new base.DecryptionError("Missing fields in input");
}
var res;
try {
res = this._olmDevice.decryptGroupMessage(
event.getRoomId(), content.sender_key, content.session_id, content.ciphertext
);
} catch (e) {
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
this._addEventToPendingList(event);
}
throw new base.DecryptionError(e);
}
if (res === null) {
// We've got a message for a session we don't have.
this._addEventToPendingList(event);
throw new base.DecryptionError(
"The sender's device has not sent us the keys for this message."
);
}
var payload = JSON.parse(res.result);
// belt-and-braces check that the room id matches that indicated by the HS
// (this is somewhat redundant, since the megolm session is scoped to the
// room, so neither the sender nor a MITM can lie about the room_id).
if (payload.room_id !== event.getRoomId()) {
throw new base.DecryptionError(
"Message intended for room " + payload.room_id
);
}
event.setClearData(payload, res.keysProved, res.keysClaimed);
};
/**
* Add an event to the list of those we couldn't decrypt the first time we
* saw them.
*
* @private
*
* @param {module:models/event.MatrixEvent} event
*/
MegolmDecryption.prototype._addEventToPendingList = function(event) {
var content = event.getWireContent();
var k = content.sender_key + "|" + content.session_id;
if (!this._pendingEvents[k]) {
this._pendingEvents[k] = [];
}
this._pendingEvents[k].push(event);
};
/**
* @inheritdoc
*
* @param {module:models/event.MatrixEvent} event key event
*/
MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
console.log("Adding key from ", event);
var content = event.getContent();
if (!content.room_id ||
!content.session_id ||
!content.session_key
) {
console.error("key event is missing fields");
return;
}
this._olmDevice.addInboundGroupSession(
content.room_id, event.getSenderKey(), content.session_id,
content.session_key, event.getKeysClaimed()
);
var k = event.getSenderKey() + "|" + content.session_id;
var pending = this._pendingEvents[k];
if (pending) {
// have another go at decrypting events sent with this session.
delete this._pendingEvents[k];
for (var i = 0; i < pending.length; i++) {
try {
this.decryptEvent(pending[i]);
console.log("successful re-decryption of", pending[i]);
} catch (e) {
console.log("Still can't decrypt", pending[i], e.stack || e);
}
}
}
};
base.registerAlgorithm(
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption
);
+319
View File
@@ -0,0 +1,319 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* Defines m.olm encryption/decryption
*
* @module crypto/algorithms/olm
*/
var q = require('q');
var utils = require("../../utils");
var olmlib = require("../olmlib");
var DeviceInfo = require("../deviceinfo");
var DeviceVerification = DeviceInfo.DeviceVerification;
var base = require("./base");
/**
* Olm encryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.EncryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.EncryptionAlgorithm}
*/
function OlmEncryption(params) {
base.EncryptionAlgorithm.call(this, params);
this._sessionPrepared = false;
this._prepPromise = null;
}
utils.inherits(OlmEncryption, base.EncryptionAlgorithm);
/**
* @private
* @param {string[]} roomMembers list of currently-joined users in the room
* @return {module:client.Promise} Promise which resolves when setup is complete
*/
OlmEncryption.prototype._ensureSession = function(roomMembers) {
if (this._prepPromise) {
// prep already in progress
return this._prepPromise;
}
if (this._sessionPrepared) {
// prep already done
return q();
}
var self = this;
this._prepPromise = self._crypto.downloadKeys(roomMembers, true).then(function(res) {
return self._crypto.ensureOlmSessionsForUsers(roomMembers);
}).then(function() {
self._sessionPrepared = true;
}).finally(function() {
self._prepPromise = null;
});
return this._prepPromise;
};
/**
* @inheritdoc
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
// pick the list of recipients based on the membership list.
//
// TODO: there is a race condition here! What if a new user turns up
// just as you are sending a secret message?
var users = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
var self = this;
return this._ensureSession(users).then(function() {
var payloadFields = {
room_id: room.roomId,
type: eventType,
content: content,
};
var encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
for (var i = 0; i < users.length; ++i) {
var userId = users[i];
var devices = self._crypto.getStoredDevicesForUser(userId);
for (var j = 0; j < devices.length; ++j) {
var deviceInfo = devices[j];
var key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
continue;
}
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
// don't bother setting up sessions with blocked users
continue;
}
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
self._userId, self._deviceId, self._olmDevice,
userId, deviceInfo, payloadFields
);
}
}
return encryptedContent;
});
};
/**
* Olm decryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.DecryptionAlgorithm}
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.DecryptionAlgorithm}
*/
function OlmDecryption(params) {
base.DecryptionAlgorithm.call(this, params);
}
utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
/**
* @inheritdoc
*
* @param {MatrixEvent} event
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
OlmDecryption.prototype.decryptEvent = function(event) {
var content = event.getWireContent();
var deviceKey = content.sender_key;
var ciphertext = content.ciphertext;
if (!ciphertext) {
throw new base.DecryptionError("Missing ciphertext");
}
if (!(this._olmDevice.deviceCurve25519Key in ciphertext)) {
throw new base.DecryptionError("Not included in recipients");
}
var message = ciphertext[this._olmDevice.deviceCurve25519Key];
var payloadString;
try {
payloadString = this._decryptMessage(deviceKey, message);
} catch (e) {
console.warn(
"Failed to decrypt Olm event (id=" +
event.getId() + ") from " + deviceKey +
": " + e.message
);
throw new base.DecryptionError("Bad Encrypted Message");
}
var payload = JSON.parse(payloadString);
// check that we were the intended recipient, to avoid unknown-key attack
// https://github.com/vector-im/vector-web/issues/2483
if (payload.recipient != this._userId) {
console.warn(
"Event " + event.getId() + ": Intended recipient " +
payload.recipient + " does not match our id " + this._userId
);
throw new base.DecryptionError(
"Message was intented for " + payload.recipient
);
}
if (payload.recipient_keys.ed25519 !=
this._olmDevice.deviceEd25519Key) {
console.warn(
"Event " + event.getId() + ": Intended recipient ed25519 key " +
payload.recipient_keys.ed25519 + " did not match ours"
);
throw new base.DecryptionError("Message not intended for this device");
}
// check that the original sender matches what the homeserver told us, to
// avoid people masquerading as others.
// (this check is also provided via the sender's embedded ed25519 key,
// which is checked elsewhere).
if (payload.sender != event.getSender()) {
console.warn(
"Event " + event.getId() + ": original sender " + payload.sender +
" does not match reported sender " + event.getSender()
);
throw new base.DecryptionError(
"Message forwarded from " + payload.sender
);
}
// Olm events intended for a room have a room_id.
if (payload.room_id !== event.getRoomId()) {
console.warn(
"Event " + event.getId() + ": original room " + payload.room_id +
" does not match reported room " + event.room_id
);
throw new base.DecryptionError(
"Message intended for room " + payload.room_id
);
}
event.setClearData(payload, {curve25519: deviceKey}, payload.keys || {});
};
/**
* Attempt to decrypt an Olm message
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key of the sender
* @param {object} message message object, with 'type' and 'body' fields
*
* @return {string} payload, if decrypted successfully.
*/
OlmDecryption.prototype._decryptMessage = function(theirDeviceIdentityKey, message) {
var sessionIds = this._olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
// try each session in turn.
var decryptionErrors = {};
for (var i = 0; i < sessionIds.length; i++) {
var sessionId = sessionIds[i];
try {
var payload = this._olmDevice.decryptMessage(
theirDeviceIdentityKey, sessionId, message.type, message.body
);
console.log(
"Decrypted Olm message from " + theirDeviceIdentityKey +
" with session " + sessionId
);
return payload;
} catch (e) {
var foundSession = this._olmDevice.matchesSession(
theirDeviceIdentityKey, sessionId, message.type, message.body
);
if (foundSession) {
// decryption failed, but it was a prekey message matching this
// session, so it should have worked.
throw new Error(
"Error decrypting prekey message with existing session id " +
sessionId + ": " + e.message
);
}
// otherwise it's probably a message for another session; carry on, but
// keep a record of the error
decryptionErrors[sessionId] = e.message;
}
}
if (message.type !== 0) {
// not a prekey message, so it should have matched an existing session, but it
// didn't work.
if (sessionIds.length === 0) {
throw new Error("No existing sessions");
}
throw new Error(
"Error decrypting non-prekey message with existing sessions: " +
JSON.stringify(decryptionErrors)
);
}
// prekey message which doesn't match any existing sessions: make a new
// session.
var res;
try {
res = this._olmDevice.createInboundSession(
theirDeviceIdentityKey, message.type, message.body
);
} catch (e) {
decryptionErrors["(new)"] = e.message;
throw new Error(
"Error decrypting prekey message: " +
JSON.stringify(decryptionErrors)
);
}
console.log(
"created new inbound Olm session ID " +
res.session_id + " with " + theirDeviceIdentityKey
);
return res.payload;
};
base.registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
+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;
+1244
View File
File diff suppressed because it is too large Load Diff
+269
View File
@@ -0,0 +1,269 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module olmlib
*
* Utilities common to olm encryption algorithms
*/
var q = require('q');
var anotherjson = require('another-json');
var utils = require("../utils");
/**
* matrix algorithm tag for olm
*/
module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
/**
* matrix algorithm tag for megolm
*/
module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
/**
* Encrypt an event payload for an Olm device
*
* @param {Object<string, string>} resultsObject The `ciphertext` property
* of the m.room.encrypted event to which to add our result
*
* @param {string} ourUserId
* @param {string} ourDeviceId
* @param {module:crypto/OlmDevice} olmDevice olm.js wrapper
* @param {string} recipientUserId
* @param {module:crypto/deviceinfo} recipientDevice
* @param {object} payloadFields fields to include in the encrypted payload
*/
module.exports.encryptMessageForDevice = function(
resultsObject,
ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice,
payloadFields
) {
var deviceKey = recipientDevice.getIdentityKey();
var sessionId = olmDevice.getSessionIdForDevice(deviceKey);
if (sessionId === null) {
// If we don't have a session for a device then
// we can't encrypt a message for it.
return;
}
console.log(
"Using sessionid " + sessionId + " for device " +
recipientUserId + ":" + recipientDevice.deviceId
);
var payload = {
sender: ourUserId,
sender_device: ourDeviceId,
// Include the Ed25519 key so that the recipient knows what
// device this message came from.
// We don't need to include the curve25519 key since the
// recipient will already know this from the olm headers.
// When combined with the device keys retrieved from the
// homeserver signed by the ed25519 key this proves that
// the curve25519 key and the ed25519 key are owned by
// the same device.
keys: {
"ed25519": olmDevice.deviceEd25519Key,
},
// include the recipient device details in the payload,
// to avoid unknown key attacks, per
// https://github.com/vector-im/vector-web/issues/2483
recipient: recipientUserId,
recipient_keys: {
"ed25519": recipientDevice.getFingerprint(),
},
};
// TODO: technically, a bunch of that stuff only needs to be included for
// pre-key messages: after that, both sides know exactly which devices are
// involved in the session. If we're looking to reduce data transfer in the
// future, we could elide them for subsequent messages.
utils.extend(payload, payloadFields);
resultsObject[deviceKey] = olmDevice.encryptMessage(
deviceKey, sessionId, JSON.stringify(payload)
);
};
/**
* Try to make sure we have established olm sessions for the given devices.
*
* @param {module:crypto/OlmDevice} olmDevice
*
* @param {module:base-apis~MatrixBaseApis} baseApis
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @return {module:client.Promise} resolves once the sessions are complete, to
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
*/
module.exports.ensureOlmSessionsForDevices = function(
olmDevice, baseApis, devicesByUser
) {
var devicesWithoutSession = [
// [userId, deviceId], ...
];
var result = {};
for (var userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) { continue; }
result[userId] = {};
var devices = devicesByUser[userId];
for (var j = 0; j < devices.length; j++) {
var deviceInfo = devices[j];
var deviceId = deviceInfo.deviceId;
var key = deviceInfo.getIdentityKey();
var sessionId = olmDevice.getSessionIdForDevice(key);
if (sessionId === null) {
devicesWithoutSession.push([userId, deviceId]);
}
result[userId][deviceId] = {
device: deviceInfo,
sessionId: sessionId,
};
}
}
if (devicesWithoutSession.length === 0) {
return q(result);
}
// TODO: this has a race condition - if we try to send another message
// while we are claiming a key, we will end up claiming two and setting up
// two sessions.
//
// That should eventually resolve itself, but it's poor form.
var oneTimeKeyAlgorithm = "signed_curve25519";
return baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm
).then(function(res) {
var otk_res = res.one_time_keys || {};
for (var userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) { continue; }
var userRes = otk_res[userId] || {};
var devices = devicesByUser[userId];
for (var j = 0; j < devices.length; j++) {
var deviceInfo = devices[j];
var deviceId = deviceInfo.deviceId;
if (result[userId][deviceId].sessionId) {
// we already have a result for this device
continue;
}
var deviceRes = userRes[deviceId] || {};
var oneTimeKey = null;
for (var keyId in deviceRes) {
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
oneTimeKey = deviceRes[keyId];
}
}
if (!oneTimeKey) {
console.warn(
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
") for device " + userId + ":" + deviceId
);
continue;
}
var sid = _verifyKeyAndStartSession(
olmDevice, oneTimeKey, userId, deviceInfo
);
result[userId][deviceId].sessionId = sid;
}
}
return result;
});
};
function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
var deviceId = deviceInfo.deviceId;
try {
_verifySignature(
olmDevice, oneTimeKey, userId, deviceId,
deviceInfo.getFingerprint()
);
} catch (e) {
console.error(
"Unable to verify signature on one-time key for device " +
userId + ":" + deviceId + ":", e
);
return null;
}
var sid;
try {
sid = olmDevice.createOutboundSession(
deviceInfo.getIdentityKey(), oneTimeKey.key
);
} catch (e) {
// possibly a bad key
console.error("Error starting session with device " +
userId + ":" + deviceId + ": " + e);
return null;
}
console.log("Started new sessionid " + sid +
" for device " + userId + ":" + deviceId);
return sid;
}
/**
* Verify the signature on an object
*
* @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op
*
* @param {Object} obj object to check signature on. Note that this will be
* stripped of its 'signatures' and 'unsigned' properties.
*
* @param {string} signingUserId ID of the user whose signature should be checked
*
* @param {string} signingDeviceId ID of the device whose signature should be checked
*
* @param {string} signingKey base64-ed ed25519 public key
*/
var _verifySignature = module.exports.verifySignature = function(
olmDevice, obj, signingUserId, signingDeviceId, signingKey
) {
var signKeyId = "ed25519:" + signingDeviceId;
var signatures = obj.signatures || {};
var userSigs = signatures[signingUserId] || {};
var signature = userSigs[signKeyId];
if (!signature) {
throw Error("No signature");
}
// prepare the canonical json: remove unsigned and signatures, and stringify with
// anotherjson
delete obj.unsigned;
delete obj.signatures;
var json = anotherjson.stringify(obj);
olmDevice.verifySignature(
signingKey, json, signature
);
};
+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;
+192
View File
@@ -0,0 +1,192 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module filter
*/
var FilterComponent = require("./filter-component");
/**
* @param {Object} obj
* @param {string} keyNesting
* @param {*} val
*/
function setProp(obj, keyNesting, val) {
var nestedKeys = keyNesting.split(".");
var currentObj = obj;
for (var i = 0; i < (nestedKeys.length - 1); i++) {
if (!currentObj[nestedKeys[i]]) {
currentObj[nestedKeys[i]] = {};
}
currentObj = currentObj[nestedKeys[i]];
}
currentObj[nestedKeys[nestedKeys.length - 1]] = val;
}
/**
* Construct a new Filter.
* @constructor
* @param {string} userId The user ID for this filter.
* @param {string=} filterId The filter ID if known.
* @prop {string} userId The user ID of the filter
* @prop {?string} filterId The filter ID
*/
function Filter(userId, filterId) {
this.userId = userId;
this.filterId = filterId;
this.definition = {};
}
/**
* Get the ID of this filter on your homeserver (if known)
* @return {?Number} The filter ID
*/
Filter.prototype.getFilterId = function() {
return this.filterId;
};
/**
* Get the JSON body of the filter.
* @return {Object} The filter definition
*/
Filter.prototype.getDefinition = function() {
return this.definition;
};
/**
* Set the JSON body of the filter
* @param {Object} definition The filter definition
*/
Filter.prototype.setDefinition = function(definition) {
this.definition = definition;
// This is all ported from synapse's FilterCollection()
// definitions look something like:
// {
// "room": {
// "rooms": ["!abcde:example.com"],
// "not_rooms": ["!123456:example.com"],
// "state": {
// "types": ["m.room.*"],
// "not_rooms": ["!726s6s6q:example.com"],
// },
// "timeline": {
// "limit": 10,
// "types": ["m.room.message"],
// "not_rooms": ["!726s6s6q:example.com"],
// "not_senders": ["@spam:example.com"]
// "contains_url": true
// },
// "ephemeral": {
// "types": ["m.receipt", "m.typing"],
// "not_rooms": ["!726s6s6q:example.com"],
// "not_senders": ["@spam:example.com"]
// }
// },
// "presence": {
// "types": ["m.presence"],
// "not_senders": ["@alice:example.com"]
// },
// "event_format": "client",
// "event_fields": ["type", "content", "sender"]
// }
var room_filter_json = definition.room;
// consider the top level rooms/not_rooms filter
var room_filter_fields = {};
if (room_filter_json) {
if (room_filter_json.rooms) {
room_filter_fields.rooms = room_filter_json.rooms;
}
if (room_filter_json.rooms) {
room_filter_fields.not_rooms = room_filter_json.not_rooms;
}
this._include_leave = room_filter_json.include_leave || false;
}
this._room_filter = new FilterComponent(room_filter_fields);
this._room_timeline_filter = new FilterComponent(
room_filter_json ? (room_filter_json.timeline || {}) : {}
);
// don't bother porting this from synapse yet:
// this._room_state_filter =
// new FilterComponent(room_filter_json.state || {});
// this._room_ephemeral_filter =
// new FilterComponent(room_filter_json.ephemeral || {});
// this._room_account_data_filter =
// new FilterComponent(room_filter_json.account_data || {});
// this._presence_filter =
// new FilterComponent(definition.presence || {});
// this._account_data_filter =
// new FilterComponent(definition.account_data || {});
};
/**
* Get the room.timeline filter component of the filter
* @return {FilterComponent} room timeline filter component
*/
Filter.prototype.getRoomTimelineFilterComponent = function() {
return this._room_timeline_filter;
};
/**
* Filter the list of events based on whether they are allowed in a timeline
* based on this filter
* @param {MatrixEvent[]} events the list of events being filtered
* @return {MatrixEvent[]} the list of events which match the filter
*/
Filter.prototype.filterRoomTimeline = function(events) {
return this._room_timeline_filter.filter(this._room_filter.filter(events));
};
/**
* Set the max number of events to return for each room's timeline.
* @param {Number} limit The max number of events to return for each room.
*/
Filter.prototype.setTimelineLimit = function(limit) {
setProp(this.definition, "room.timeline.limit", limit);
};
/**
* Control whether left rooms should be included in responses.
* @param {boolean} includeLeave True to make rooms the user has left appear
* in responses.
*/
Filter.prototype.setIncludeLeaveRooms = function(includeLeave) {
setProp(this.definition, "room.include_leave", includeLeave);
};
/**
* Create a filter from existing data.
* @static
* @param {string} userId
* @param {string} filterId
* @param {Object} jsonObj
* @return {Filter}
*/
Filter.fromJson = function(userId, filterId, jsonObj) {
var filter = new Filter(userId, filterId);
filter.setDefinition(jsonObj);
return filter;
};
/** The Filter class */
module.exports = Filter;
+470 -164
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module. See {@link MatrixHttpApi} for the public class.
@@ -6,6 +21,11 @@
var q = require("q");
var utils = require("./utils");
// we use our own implementation of setTimeout, so that if we get suspended in
// the middle of a /sync, we cancel the sync as soon as we awake, rather than
// waiting for the delay to elapse.
var callbacks = require("./realtime-callbacks");
/*
TODO:
- CS: complete register function (doing stages)
@@ -13,122 +33,56 @@ TODO:
*/
/**
* A constant representing the URI path for version 1 of the Client-Server HTTP API.
* A constant representing the URI path for release 0 of the Client-Server HTTP API.
*/
module.exports.PREFIX_V1 = "/_matrix/client/api/v1";
module.exports.PREFIX_R0 = "/_matrix/client/r0";
/**
* A constant representing the URI path for version 2 alpha of the Client-Server
* HTTP API.
* A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
*/
module.exports.PREFIX_V2_ALPHA = "/_matrix/client/v2_alpha";
module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable";
/**
* URI path for the identity API
*/
module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
/**
* URI path for the media repo API
*/
module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
/**
* Construct a MatrixHttpApi.
* @constructor
* @param {EventEmitter} event_emitter The event emitter to use for emitting events
* @param {Object} opts The options to use for this HTTP API.
* @param {string} opts.baseUrl Required. The base client-server URL e.g.
* 'http://localhost:8008'.
* @param {Function} opts.request Required. The function to call for HTTP
* requests. This function must look like function(opts, callback){ ... }.
* @param {string} opts.prefix Required. The matrix client prefix to use, e.g.
* '/_matrix/client/api/v1'. See PREFIX_V1 and PREFIX_V2_ALPHA for constants.
* @param {bool} opts.onlyData True to return only the 'data' component of the
* response (e.g. the parsed HTTP body). If false, requests will return status
* codes and headers in addition to data. Default: false.
* '/_matrix/client/r0'. See PREFIX_R0 and PREFIX_UNSTABLE for constants.
*
* @param {bool=} opts.onlyData True to return only the 'data' component of the
* response (e.g. the parsed HTTP body). If false, requests will return an
* object with the properties <tt>code</tt>, <tt>headers</tt> and <tt>data</tt>.
*
* @param {string} opts.accessToken The access_token to send with requests. Can be
* null to not send an access token.
* @param {Object} opts.extraParams Optional. Extra query parameters to send on
* requests.
*/
module.exports.MatrixHttpApi = function MatrixHttpApi(opts) {
module.exports.MatrixHttpApi = function MatrixHttpApi(event_emitter, opts) {
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
opts.onlyData = opts.onlyData || false;
this.event_emitter = event_emitter;
this.opts = opts;
this.uploads = [];
};
module.exports.MatrixHttpApi.prototype = {
// URI functions
// =============
/**
* Get the HTTP URL for an MXC URI.
* @param {string} mxc The mxc:// URI.
* @param {Number} width The desired width of the thumbnail.
* @param {Number} height The desired height of the thumbnail.
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @return {string} The complete URL to the content.
*/
getHttpUriForMxc: function(mxc, width, height, resizeMethod) {
if (typeof mxc !== "string" || !mxc) {
return mxc;
}
if (mxc.indexOf("mxc://") !== 0) {
return mxc;
}
var serverAndMediaId = mxc.slice(6); // strips mxc://
var prefix = "/_matrix/media/v1/download/";
var params = {};
if (width) {
params.width = width;
}
if (height) {
params.height = height;
}
if (resizeMethod) {
params.method = resizeMethod;
}
if (utils.keys(params).length > 0) {
// these are thumbnailing params so they probably want the
// thumbnailing API...
prefix = "/_matrix/media/v1/thumbnail/";
}
var fragmentOffset = serverAndMediaId.indexOf("#"),
fragment = "";
if (fragmentOffset >= 0) {
fragment = serverAndMediaId.substr(fragmentOffset);
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
}
return this.opts.baseUrl + prefix + serverAndMediaId +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params))) + fragment;
},
/**
* Get an identicon URL from an arbitrary string.
* @param {string} identiconString The string to create an identicon for.
* @param {Number} width The desired width of the image in pixels.
* @param {Number} height The desired height of the image in pixels.
* @return {string} The complete URL to the identicon.
*/
getIdenticonUri: function(identiconString, width, height) {
if (!identiconString) {
return;
}
if (!width) { width = 96; }
if (!height) { height = 96; }
var params = {
width: width,
height: height
};
var path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
$ident: identiconString
});
return this.opts.baseUrl + path +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params)));
},
/**
* Get the content repository url with query parameters.
* @return {Object} An object with a 'base', 'path' and 'params' for base URL,
@@ -147,22 +101,87 @@ module.exports.MatrixHttpApi.prototype = {
/**
* Upload content to the Home Server
* @param {File} file A File object (in a browser) or in Node,
an object with properties:
name: The file's name
stream: A read stream
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
*
* @param {object} file The object to upload. On a browser, something that
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
* a Buffer, String or ReadStream.
*
* @param {object} opts options object
*
* @param {string=} opts.name Name to give the file on the server. Defaults
* to <tt>file.name</tt>.
*
* @param {string=} opts.type Content-type for the upload. Defaults to
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
*
* @param {boolean=} opts.rawResponse Return the raw body, rather than
* parsing the JSON. Defaults to false (except on node.js, where it
* defaults to true for backwards compatibility).
*
* @param {boolean=} opts.onlyContentUri Just return the content URI,
* rather than the whole body. Defaults to false (except on browsers,
* where it defaults to true for backwards compatibility). Ignored if
* opts.rawResponse is true.
*
* @param {Function=} opts.callback Deprecated. Optional. The callback to
* invoke on success/failure. See the promise return values for more
* information.
*
* @return {module:client.Promise} Resolves to response object, as
* determined by this.opts.onlyData, opts.rawResponse, and
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
*/
uploadContent: function(file, callback) {
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
);
uploadContent: function(file, opts) {
if (utils.isFunction(opts)) {
// opts used to be callback
opts = {
callback: opts,
};
} else if (opts === undefined) {
opts = {};
}
var defer = q.defer();
var url = this.opts.baseUrl + "/_matrix/media/v1/upload";
// if the file doesn't have a mime type, use a default since
// the HS errors if we don't supply one.
var contentType = opts.type || file.type || 'application/octet-stream';
var fileName = opts.name || file.name;
// we used to recommend setting file.stream to the thing to upload on
// nodejs.
var body = file.stream ? file.stream : file;
// backwards-compatibility hacks where we used to do different things
// between browser and node.
var rawResponse = opts.rawResponse;
if (rawResponse === undefined) {
if (global.XMLHttpRequest) {
rawResponse = false;
} else {
console.warn(
"Returning the raw JSON from uploadContent(). Future " +
"versions of the js-sdk will change this default, to " +
"return the parsed object. Set opts.rawResponse=false " +
"to change this behaviour now."
);
rawResponse = true;
}
}
var onlyContentUri = opts.onlyContentUri;
if (!rawResponse && onlyContentUri === undefined) {
if (global.XMLHttpRequest) {
console.warn(
"Returning only the content-uri from uploadContent(). " +
"Future versions of the js-sdk will change this " +
"default, to return the whole response object. Set " +
"opts.onlyContentUri=false to change this behaviour now."
);
onlyContentUri = true;
} else {
onlyContentUri = false;
}
}
// browser-request doesn't support File objects because it deep-copies
// the options using JSON.parse(JSON.stringify(options)). Instead of
// loading the whole file into memory as a string and letting
@@ -170,64 +189,129 @@ module.exports.MatrixHttpApi.prototype = {
// use XMLHttpRequest directly.
// (browser-request doesn't support progress either, which is also kind
// of important here)
var upload = { loaded: 0, total: 0 };
var promise;
// XMLHttpRequest doesn't parse JSON for us. request normally does, but
// we're setting opts.json=false so that it doesn't JSON-encode the
// request, which also means it doesn't JSON-decode the response. Either
// way, we have to JSON-parse the response ourselves.
var bodyParser = null;
if (!rawResponse) {
bodyParser = function(rawBody) {
var body = JSON.parse(rawBody);
if (onlyContentUri) {
body = body.content_uri;
if (body === undefined) {
throw Error('Bad response');
}
}
return body;
};
}
if (global.XMLHttpRequest) {
var defer = q.defer();
var xhr = new global.XMLHttpRequest();
var cb = requestCallback(defer, callback, this.opts.onlyData);
upload.xhr = xhr;
var cb = requestCallback(defer, opts.callback, this.opts.onlyData);
var timeout_fn = function() {
xhr.abort();
cb(new Error('Timeout'));
};
xhr.timeout_timer = setTimeout(timeout_fn, 30000);
// set an initial timeout of 30s; we'll advance it each time we get
// a progress notification
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
xhr.onreadystatechange = function() {
switch (xhr.readyState) {
case global.XMLHttpRequest.DONE:
clearTimeout(xhr.timeout_timer);
var resp = JSON.parse(xhr.responseText);
if (resp.content_uri === undefined) {
cb(new Error('Bad response'));
callbacks.clearTimeout(xhr.timeout_timer);
var resp;
try {
if (!xhr.responseText) {
throw new Error('No response body.');
}
resp = xhr.responseText;
if (bodyParser) {
resp = bodyParser(resp);
}
} catch (err) {
err.http_status = xhr.status;
cb(err);
return;
}
cb(undefined, xhr, resp.content_uri);
cb(undefined, xhr, resp);
break;
}
};
xhr.upload.addEventListener("progress", function(ev) {
clearTimeout(xhr.timeout_timer);
xhr.timeout_timer = setTimeout(timeout_fn, 30000);
callbacks.clearTimeout(xhr.timeout_timer);
upload.loaded = ev.loaded;
upload.total = ev.total;
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
defer.notify(ev);
});
var url = this.opts.baseUrl + "/_matrix/media/v1/upload";
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
url += "&filename=" + encodeURIComponent(file.name);
url += "&filename=" + encodeURIComponent(fileName);
xhr.open("POST", url);
if (file.type) {
xhr.setRequestHeader("Content-Type", file.type);
} else {
// if the file doesn't have a mime type, use a default since
// the HS errors if we don't supply one.
xhr.setRequestHeader("Content-Type", 'application/octet-stream');
}
xhr.send(file);
xhr.setRequestHeader("Content-Type", contentType);
xhr.send(body);
promise = defer.promise;
// dirty hack (as per _request) to allow the upload to be cancelled.
promise.abort = xhr.abort.bind(xhr);
} else {
var queryParams = {
filename: file.name,
access_token: this.opts.accessToken
filename: fileName,
};
file.stream.pipe(
this.opts.request({
uri: url,
qs: queryParams,
method: "POST"
}, requestCallback(defer, callback, this.opts.onlyData))
promise = this.authedRequest(
opts.callback, "POST", "/upload", queryParams, body, {
prefix: "/_matrix/media/v1",
headers: {"Content-Type": contentType},
json: false,
bodyParser: bodyParser,
}
);
}
return defer.promise;
var self = this;
// remove the upload from the list on completion
var promise0 = promise.finally(function() {
for (var i = 0; i < self.uploads.length; ++i) {
if (self.uploads[i] === upload) {
self.uploads.splice(i, 1);
return;
}
}
});
// copy our dirty abort() method to the new promise
promise0.abort = promise.abort;
upload.promise = promise0;
this.uploads.push(upload);
return promise0;
},
cancelUpload: function(promise) {
if (promise.abort) {
promise.abort();
return true;
}
return false;
},
getCurrentUploads: function() {
return this.uploads;
},
idServerRequest: function(callback, method, path, params, prefix) {
@@ -257,7 +341,12 @@ module.exports.MatrixHttpApi.prototype = {
opts,
requestCallback(defer, callback, this.opts.onlyData)
);
return defer.promise;
// ID server does not always take JSON, so we can't use requests' 'json'
// option as we do with the home server, but it does return JSON, so
// parse it manually
return defer.promise.then(function(response) {
return JSON.parse(response);
});
},
/**
@@ -267,9 +356,22 @@ module.exports.MatrixHttpApi.prototype = {
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
*
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
*
* @param {Object=} opts additional options
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
@@ -277,10 +379,28 @@ module.exports.MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
authedRequest: function(callback, method, path, queryParams, data) {
if (!queryParams) { queryParams = {}; }
queryParams.access_token = this.opts.accessToken;
return this.request(callback, method, path, queryParams, data);
authedRequest: function(callback, method, path, queryParams, data, opts) {
if (!queryParams) {
queryParams = {};
}
if (!queryParams.access_token) {
queryParams.access_token = this.opts.accessToken;
}
var request_promise = this.request(
callback, method, path, queryParams, data, opts
);
var self = this;
request_promise.catch(function(err) {
if (err.errcode == 'M_UNKNOWN_TOKEN') {
self.event_emitter.emit("Session.logged_out");
}
});
// return the original promise, otherwise tests break due to it having to
// go around the event loop one more time to process the result of the request
return request_promise;
},
/**
@@ -290,9 +410,22 @@ module.exports.MatrixHttpApi.prototype = {
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
*
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
*
* @param {Object=} opts additional options
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
@@ -300,9 +433,13 @@ module.exports.MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
request: function(callback, method, path, queryParams, data) {
return this.requestWithPrefix(
callback, method, path, queryParams, data, this.opts.prefix
request: function(callback, method, path, queryParams, data, opts) {
opts = opts || {};
var prefix = opts.prefix !== undefined ? opts.prefix : this.opts.prefix;
var fullUri = this.opts.baseUrl + prefix + path;
return this.requestOtherUrl(
callback, method, fullUri, queryParams, data, opts
);
},
@@ -320,21 +457,25 @@ module.exports.MatrixHttpApi.prototype = {
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*
* @deprecated prefer authedRequest with opts.prefix
*/
authedRequestWithPrefix: function(callback, method, path, queryParams, data,
prefix) {
var fullUri = this.opts.baseUrl + prefix + path;
if (!queryParams) {
queryParams = {};
}
queryParams.access_token = this.opts.accessToken;
return this._request(callback, method, fullUri, queryParams, data);
prefix, localTimeoutMs) {
return this.authedRequest(
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
}
);
},
/**
@@ -351,6 +492,49 @@ module.exports.MatrixHttpApi.prototype = {
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*
* @deprecated prefer request with opts.prefix
*/
requestWithPrefix: function(callback, method, path, queryParams, data, prefix,
localTimeoutMs) {
return this.request(
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
}
);
},
/**
* Perform a request to an arbitrary URL.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} uri The HTTP URI
*
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
*
* @param {Object=} opts additional options
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
@@ -358,43 +542,144 @@ module.exports.MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
requestWithPrefix: function(callback, method, path, queryParams, data, prefix) {
var fullUri = this.opts.baseUrl + prefix + path;
if (!queryParams) {
queryParams = {};
requestOtherUrl: function(callback, method, uri, queryParams, data,
opts) {
if (opts === undefined || opts === null) {
opts = {};
} else if (isFinite(opts)) {
// opts used to be localTimeoutMs
opts = {
localTimeoutMs: opts
};
}
return this._request(callback, method, fullUri, queryParams, data);
return this._request(
callback, method, uri, queryParams, data, opts
);
},
_request: function(callback, method, uri, queryParams, data) {
/**
* Form and return a homeserver request URL based on the given path
* params and prefix.
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @return {string} URL
*/
getUrl: function(path, queryParams, prefix) {
var queryString = "";
if (queryParams) {
queryString = "?" + utils.encodeParams(queryParams);
}
return this.opts.baseUrl + prefix + path + queryString;
},
/**
* @private
*
* @param {function} callback
* @param {string} method
* @param {string} uri
* @param {object} queryParams
* @param {object|string} data
* @param {object=} opts
*
* @param {boolean} [opts.json =true] Json-encode data before sending, and
* decode response on receipt. (We will still json-decode error
* responses, even if this is false.)
*
* @param {object=} opts.headers extra request headers
*
* @param {number=} opts.localTimeoutMs client-side timeout for the
* request. No timeout if undefined.
*
* @param {function=} opts.bodyParser function to parse the body of the
* response before passing it to the promise and callback.
*
* @return {module:client.Promise} a promise which resolves to either the
* response object (if this.opts.onlyData is truthy), or the parsed
* body. Rejects
*/
_request: function(callback, method, uri, queryParams, data, opts) {
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
);
}
if (!queryParams) {
queryParams = {};
}
opts = opts || {};
var self = this;
if (this.opts.extraParams) {
for (var key in this.opts.extraParams) {
if (!this.opts.extraParams.hasOwnProperty(key)) { continue; }
queryParams[key] = this.opts.extraParams[key];
}
}
var json = opts.json === undefined ? true : opts.json;
var defer = q.defer();
var timeoutId;
var timedOut = false;
var req;
var localTimeoutMs = opts.localTimeoutMs;
if (localTimeoutMs) {
timeoutId = callbacks.setTimeout(function() {
timedOut = true;
if (req && req.abort) {
req.abort();
}
defer.reject(new module.exports.MatrixError({
error: "Locally timed out waiting for a response",
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
timeout: localTimeoutMs
}));
}, localTimeoutMs);
}
var reqPromise = defer.promise;
try {
this.opts.request(
req = this.opts.request(
{
uri: uri,
method: method,
withCredentials: false,
qs: queryParams,
body: data,
json: true,
json: json,
timeout: localTimeoutMs,
headers: opts.headers || {},
_matrix_opts: this.opts
},
requestCallback(defer, callback, this.opts.onlyData)
function(err, response, body) {
if (localTimeoutMs) {
callbacks.clearTimeout(timeoutId);
if (timedOut) {
return; // already rejected promise
}
}
// if json is falsy, we won't parse any error response, so need
// to do so before turning it into a MatrixError
var parseErrorJson = !json;
var handlerFn = requestCallback(
defer, callback, self.opts.onlyData,
parseErrorJson,
opts.bodyParser
);
handlerFn(err, response, body);
}
);
if (req && req.abort) {
// FIXME: This is EVIL, but I can't think of a better way to expose
// abort() operations on underlying HTTP requests :(
reqPromise.abort = req.abort.bind(req);
}
}
catch (ex) {
defer.reject(ex);
@@ -402,7 +687,7 @@ module.exports.MatrixHttpApi.prototype = {
callback(ex);
}
}
return defer.promise;
return reqPromise;
}
};
@@ -413,14 +698,34 @@ module.exports.MatrixHttpApi.prototype = {
*
* If onlyData is true, the defer/callback is invoked with the body of the
* response, otherwise the result code.
*
* If parseErrorJson is true, we will JSON.parse the body if we get a 4xx error.
*
*/
var requestCallback = function(defer, userDefinedCallback, onlyData) {
var requestCallback = function(
defer, userDefinedCallback, onlyData,
parseErrorJson, bodyParser
) {
userDefinedCallback = userDefinedCallback || function() {};
return function(err, response, body) {
if (!err && response.statusCode >= 400) {
err = new module.exports.MatrixError(body);
err.httpStatus = response.statusCode;
if (!err) {
try {
if (response.statusCode >= 400) {
if (parseErrorJson) {
// we won't have json-decoded the response.
body = JSON.parse(body);
}
err = new module.exports.MatrixError(body);
} else if (bodyParser) {
body = bodyParser(body);
}
} catch (e) {
err = e;
}
if (err) {
err.httpStatus = response.statusCode;
}
}
if (err) {
@@ -451,6 +756,7 @@ var requestCallback = function(defer, userDefinedCallback, onlyData) {
* @prop {integer} httpStatus The numeric HTTP status code given
*/
module.exports.MatrixError = function MatrixError(errorJson) {
errorJson = errorJson || {};
this.errcode = errorJson.errcode;
this.name = errorJson.errcode || "Unknown error code";
this.message = errorJson.error || "Unknown message";
+228
View File
@@ -0,0 +1,228 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/** @module interactive-auth */
var q = require("q");
var utils = require("./utils");
/**
* Abstracts the logic used to drive the interactive auth process.
*
* <p>Components implementing an interactive auth flow should instantiate one of
* these, passing in the necessary callbacks to the constructor. They should
* then call attemptAuth, which will return a promise which will resolve or
* reject when the interactive-auth process completes.
*
* <p>Meanwhile, calls will be made to the startAuthStage and doRequest
* callbacks, and information gathered from the user can be submitted with
* submitAuthDict.
*
* @constructor
* @alias module:interactive-auth
*
* @param {object} opts options object
*
* @param {object?} opts.authData error response from the last request. If
* null, a request will be made with no auth before starting.
*
* @param {function(object?): module:client.Promise} opts.doRequest
* called with the new auth dict to submit the request. Should return a
* promise which resolves to the successful response or rejects with a
* MatrixError.
*
* @param {function(string, object?)} opts.startAuthStage
* called to ask the UI to start a particular auth stage. The arguments
* are: the login type (eg m.login.password); and (if the last request
* returned an error), an error object, with fields 'errcode' and 'error'.
*
*/
function InteractiveAuth(opts) {
this._data = opts.authData;
this._requestCallback = opts.doRequest;
this._startAuthStageCallback = opts.startAuthStage;
this._completionDeferred = null;
}
InteractiveAuth.prototype = {
/**
* begin the authentication process.
*
* @return {module:client.Promise} which resolves to the response on success,
* or rejects with the error on failure.
*/
attemptAuth: function() {
this._completionDeferred = q.defer();
if (!this._data) {
this._doRequest(null);
} else {
this._startNextAuthStage();
}
return this._completionDeferred.promise;
},
/**
* get the auth session ID
*
* @return {string} session id
*/
getSessionId: function() {
return this._data ? this._data.session : undefined;
},
/**
* get the server params for a given stage
*
* @param {string} login type for the stage
* @return {object?} any parameters from the server for this stage
*/
getStageParams: function(loginType) {
var params = {};
if (this._data && this._data.params) {
params = this._data.params;
}
return params[loginType];
},
/**
* submit a new auth dict and fire off the request. This will either
* make attemptAuth resolve/reject, or cause the startAuthStage callback
* to be called for a new stage.
*
* @param {object} authData new auth dict to send to the server. Should
* include a `type` propterty denoting the login type, as well as any
* other params for that stage.
*/
submitAuthDict: function(authData) {
if (!this._completionDeferred) {
throw new Error("submitAuthDict() called before attemptAuth()");
}
// use the sessionid from the last request.
var auth = {
session: this._data.session,
};
utils.extend(auth, authData);
this._doRequest(auth);
},
/**
* Fire off a request, and either resolve the promise, or call
* startAuthStage.
*
* @private
* @param {object?} auth new auth dict, including session id
*/
_doRequest: function(auth) {
var self = this;
// hackery to make sure that synchronous exceptions end up in the catch
// handler (without the additional event loop entailed by q.fcall or an
// extra q().then)
var prom;
try {
prom = this._requestCallback(auth);
} catch (e) {
prom = q.reject(e);
}
prom.then(
function(result) {
console.log("result from request: ", result);
self._completionDeferred.resolve(result);
}, function(error) {
if (error.httpStatus !== 401 || !error.data || !error.data.flows) {
// doesn't look like an interactive-auth failure. fail the whole lot.
throw error;
}
self._data = error.data;
self._startNextAuthStage();
}
).catch(this._completionDeferred.reject).done();
},
/**
* Pick the next stage and call the callback
*
* @private
*/
_startNextAuthStage: function() {
var nextStage = this._chooseStage();
if (!nextStage) {
throw new Error("No incomplete flows from the server");
}
var stageError = null;
if (this._data.errcode || this._data.error) {
stageError = {
errcode: this._data.errcode || "",
error: this._data.error || "",
};
}
this._startAuthStageCallback(nextStage, stageError);
},
/**
* Pick the next auth stage
*
* @private
* @return {string?} login type
*/
_chooseStage: function() {
var flow = this._chooseFlow();
console.log("Active flow => %s", JSON.stringify(flow));
var nextStage = this._firstUncompletedStage(flow);
console.log("Next stage: %s", nextStage);
return nextStage;
},
/**
* Pick one of the flows from the returned list
*
* @private
* @return {object} flow
*/
_chooseFlow: function() {
var flows = this._data.flows || [];
// always use the first flow for now
return flows[0];
},
/**
* Get the first uncompleted stage in the given flow
*
* @private
* @param {object} flow
* @return {string} login type
*/
_firstUncompletedStage: function(flow) {
var completed = (this._data || {}).completed || [];
for (var i = 0; i < flow.stages.length; ++i) {
var stageType = flow.stages[i];
if (completed.indexOf(stageType) === -1) {
return stageType;
}
}
},
};
/** */
module.exports = InteractiveAuth;
+64 -3
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/** The {@link module:models/event.MatrixEvent|MatrixEvent} class. */
@@ -15,9 +30,13 @@ module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi;
module.exports.MatrixError = require("./http-api").MatrixError;
/** The {@link module:client.MatrixClient|MatrixClient} class. */
module.exports.MatrixClient = require("./client").MatrixClient;
/** The {@link module:models/room~Room|Room} class. */
/** The {@link module:models/room|Room} class. */
module.exports.Room = require("./models/room");
/** The {@link module:models/room-member~RoomMember|RoomMember} class. */
/** The {@link module:models/event-timeline~EventTimeline} class. */
module.exports.EventTimeline = require("./models/event-timeline");
/** The {@link module:models/event-timeline-set~EventTimelineSet} class. */
module.exports.EventTimelineSet = require("./models/event-timeline-set");
/** The {@link module:models/room-member|RoomMember} class. */
module.exports.RoomMember = require("./models/room-member");
/** The {@link module:models/room-state~RoomState|RoomState} class. */
module.exports.RoomState = require("./models/room-state");
@@ -30,6 +49,15 @@ module.exports.MatrixScheduler = require("./scheduler");
module.exports.WebStorageSessionStore = require("./store/session/webstorage");
/** True if crypto libraries are being used on this client. */
module.exports.CRYPTO_ENABLED = require("./client").CRYPTO_ENABLED;
/** {@link module:content-repo|ContentRepo} utility functions. */
module.exports.ContentRepo = require("./content-repo");
/** The {@link module:filter~Filter|Filter} class. */
module.exports.Filter = require("./filter");
/** The {@link module:timeline-window~TimelineWindow} class. */
module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
/** The {@link module:interactive-auth} class. */
module.exports.InteractiveAuth = require("./interactive-auth");
/**
* Create a new Matrix Call.
@@ -54,6 +82,27 @@ module.exports.request = function(r) {
request = r;
};
/**
* Return the currently-set request function.
* @return {requestFunction} The current request function.
*/
module.exports.getRequest = function() {
return request;
};
/**
* Apply wrapping code around the request function. The wrapper function is
* installed as the new request handler, and when invoked it is passed the
* previous value, along with the options and callback arguments.
* @param {requestWrapperFunction} wrapper The wrapping function.
*/
module.exports.wrapRequest = function(wrapper) {
var origRequest = request;
request = function(options, callback) {
return wrapper(origRequest, options, callback);
};
};
/**
* Construct a Matrix Client. Similar to {@link module:client~MatrixClient}
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
@@ -77,7 +126,9 @@ module.exports.createClient = function(opts) {
};
}
opts.request = opts.request || request;
opts.store = opts.store || new module.exports.MatrixInMemoryStore();
opts.store = opts.store || new module.exports.MatrixInMemoryStore({
localStorage: global.localStorage
});
opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler();
return new module.exports.MatrixClient(opts);
};
@@ -99,6 +150,16 @@ module.exports.createClient = function(opts) {
* @param {requestCallback} callback The request callback.
*/
/**
* A wrapper for the request function interface.
* @callback requestWrapperFunction
* @param {requestFunction} origRequest The underlying request function being
* wrapped
* @param {Object} opts The options for this HTTP request, given in the same
* form as {@link requestFunction}.
* @param {requestCallback} callback The request callback.
*/
/**
* The request callback interface for performing HTTP requests. This matches the
* API for the {@link https://github.com/request/request#requestoptions-callback|
+119
View File
@@ -0,0 +1,119 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/event-context
*/
/**
* Construct a new EventContext
*
* An eventcontext is used for circumstances such as search results, when we
* have a particular event of interest, and a bunch of events before and after
* it.
*
* It also stores pagination tokens for going backwards and forwards in the
* timeline.
*
* @param {MatrixEvent} ourEvent the event at the centre of this context
*
* @constructor
*/
function EventContext(ourEvent) {
this._timeline = [ourEvent];
this._ourEventIndex = 0;
this._paginateTokens = {b: null, f: null};
// this is used by MatrixClient to keep track of active requests
this._paginateRequests = {b: null, f: null};
}
/**
* Get the main event of interest
*
* This is a convenience function for getTimeline()[getOurEventIndex()].
*
* @return {MatrixEvent} The event at the centre of this context.
*/
EventContext.prototype.getEvent = function() {
return this._timeline[this._ourEventIndex];
};
/**
* Get the list of events in this context
*
* @return {Array} An array of MatrixEvents
*/
EventContext.prototype.getTimeline = function() {
return this._timeline;
};
/**
* Get the index in the timeline of our event
*
* @return {Number}
*/
EventContext.prototype.getOurEventIndex = function() {
return this._ourEventIndex;
};
/**
* Get a pagination token.
*
* @param {boolean} backwards true to get the pagination token for going
* backwards in time
* @return {string}
*/
EventContext.prototype.getPaginateToken = function(backwards) {
return this._paginateTokens[backwards ? 'b' : 'f'];
};
/**
* Set a pagination token.
*
* Generally this will be used only by the matrix js sdk.
*
* @param {string} token pagination token
* @param {boolean} backwards true to set the pagination token for going
* backwards in time
*/
EventContext.prototype.setPaginateToken = function(token, backwards) {
this._paginateTokens[backwards ? 'b' : 'f'] = token;
};
/**
* Add more events to the timeline
*
* @param {Array} events new events, in timeline order
* @param {boolean} atStart true to insert new events at the start
*/
EventContext.prototype.addEvents = function(events, atStart) {
// TODO: should we share logic with Room.addEventsToTimeline?
// Should Room even use EventContext?
if (atStart) {
this._timeline = events.concat(this._timeline);
this._ourEventIndex += events.length;
} else {
this._timeline = this._timeline.concat(events);
}
};
/**
* The EventContext class
*/
module.exports = EventContext;
+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
*/
+338
View File
@@ -0,0 +1,338 @@
"use strict";
/**
* @module models/event-timeline
*/
var RoomState = require("./room-state");
var utils = require("../utils");
var MatrixEvent = require("./event").MatrixEvent;
/**
* Construct a new EventTimeline
*
* <p>An EventTimeline represents a contiguous sequence of events in a room.
*
* <p>As well as keeping track of the events themselves, it stores the state of
* the room at the beginning and end of the timeline, and pagination tokens for
* going backwards and forwards in the timeline.
*
* <p>In order that clients can meaningfully maintain an index into a timeline,
* the EventTimeline object tracks a 'baseIndex'. This starts at zero, but is
* incremented when events are prepended to the timeline. The index of an event
* relative to baseIndex therefore remains constant.
*
* <p>Once a timeline joins up with its neighbour, they are linked together into a
* doubly-linked list.
*
* @param {EventTimelineSet} eventTimelineSet the set of timelines this is part of
* @constructor
*/
function EventTimeline(eventTimelineSet) {
this._eventTimelineSet = eventTimelineSet;
this._roomId = eventTimelineSet.room ? eventTimelineSet.room.roomId : null;
this._events = [];
this._baseIndex = 0;
this._startState = new RoomState(this._roomId);
this._startState.paginationToken = null;
this._endState = new RoomState(this._roomId);
this._endState.paginationToken = null;
this._prevTimeline = null;
this._nextTimeline = null;
// this is used by client.js
this._paginationRequests = {'b': null, 'f': null};
this._name = this._roomId + ":" + new Date().toISOString();
}
/**
* Symbolic constant for methods which take a 'direction' argument:
* refers to the start of the timeline, or backwards in time.
*/
EventTimeline.BACKWARDS = "b";
/**
* Symbolic constant for methods which take a 'direction' argument:
* refers to the end of the timeline, or forwards in time.
*/
EventTimeline.FORWARDS = "f";
/**
* Initialise the start and end state with the given events
*
* <p>This can only be called before any events are added.
*
* @param {MatrixEvent[]} stateEvents list of state events to initialise the
* state with.
* @throws {Error} if an attempt is made to call this after addEvent is called.
*/
EventTimeline.prototype.initialiseState = function(stateEvents) {
if (this._events.length > 0) {
throw new Error("Cannot initialise state after events are added");
}
// we deep-copy the events here, in case they get changed later - we don't
// want changes to the start state leaking through to the end state.
var oldStateEvents = utils.map(
utils.deepCopy(
stateEvents.map(function(mxEvent) { return mxEvent.event; })
), function(ev) { return new MatrixEvent(ev); });
this._startState.setStateEvents(oldStateEvents);
this._endState.setStateEvents(stateEvents);
};
/**
* Get the ID of the room for this timeline
* @return {string} room ID
*/
EventTimeline.prototype.getRoomId = function() {
return this._roomId;
};
/**
* Get the filter for this timeline's timelineSet (if any)
* @return {Filter} filter
*/
EventTimeline.prototype.getFilter = function() {
return this._eventTimelineSet.getFilter();
};
/**
* Get the timelineSet for this timeline
* @return {EventTimelineSet} timelineSet
*/
EventTimeline.prototype.getTimelineSet = function() {
return this._eventTimelineSet;
};
/**
* Get the base index.
*
* <p>This is an index which is incremented when events are prepended to the
* timeline. An individual event therefore stays at the same index in the array
* relative to the base index (although note that a given event's index may
* well be less than the base index, thus giving that event a negative relative
* index).
*
* @return {number}
*/
EventTimeline.prototype.getBaseIndex = function() {
return this._baseIndex;
};
/**
* Get the list of events in this context
*
* @return {MatrixEvent[]} An array of MatrixEvents
*/
EventTimeline.prototype.getEvents = function() {
return this._events;
};
/**
* Get the room state at the start/end of the timeline
*
* @param {string} direction EventTimeline.BACKWARDS to get the state at the
* start of the timeline; EventTimeline.FORWARDS to get the state at the end
* of the timeline.
*
* @return {RoomState} state at the start/end of the timeline
*/
EventTimeline.prototype.getState = function(direction) {
if (direction == EventTimeline.BACKWARDS) {
return this._startState;
} else if (direction == EventTimeline.FORWARDS) {
return this._endState;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
};
/**
* Get a pagination token
*
* @param {string} direction EventTimeline.BACKWARDS to get the pagination
* token for going backwards in time; EventTimeline.FORWARDS to get the
* pagination token for going forwards in time.
*
* @return {?string} pagination token
*/
EventTimeline.prototype.getPaginationToken = function(direction) {
return this.getState(direction).paginationToken;
};
/**
* Set a pagination token
*
* @param {?string} token pagination token
*
* @param {string} direction EventTimeline.BACKWARDS to set the pagination
* token for going backwards in time; EventTimeline.FORWARDS to set the
* pagination token for going forwards in time.
*/
EventTimeline.prototype.setPaginationToken = function(token, direction) {
this.getState(direction).paginationToken = token;
};
/**
* Get the next timeline in the series
*
* @param {string} direction EventTimeline.BACKWARDS to get the previous
* timeline; EventTimeline.FORWARDS to get the next timeline.
*
* @return {?EventTimeline} previous or following timeline, if they have been
* joined up.
*/
EventTimeline.prototype.getNeighbouringTimeline = function(direction) {
if (direction == EventTimeline.BACKWARDS) {
return this._prevTimeline;
} else if (direction == EventTimeline.FORWARDS) {
return this._nextTimeline;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
};
/**
* Set the next timeline in the series
*
* @param {EventTimeline} neighbour previous/following timeline
*
* @param {string} direction EventTimeline.BACKWARDS to set the previous
* timeline; EventTimeline.FORWARDS to set the next timeline.
*
* @throws {Error} if an attempt is made to set the neighbouring timeline when
* it is already set.
*/
EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction) {
if (this.getNeighbouringTimeline(direction)) {
throw new Error("timeline already has a neighbouring timeline - " +
"cannot reset neighbour");
}
if (direction == EventTimeline.BACKWARDS) {
this._prevTimeline = neighbour;
} else if (direction == EventTimeline.FORWARDS) {
this._nextTimeline = neighbour;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
// make sure we don't try to paginate this timeline
this.setPaginationToken(null, direction);
};
/**
* Add a new event to the timeline, and update the state
*
* @param {MatrixEvent} event new event
* @param {boolean} atStart true to insert new event at the start
*/
EventTimeline.prototype.addEvent = function(event, atStart) {
var stateContext = atStart ? this._startState : this._endState;
// only call setEventMetadata on the unfiltered timelineSets
var timelineSet = this.getTimelineSet();
if (timelineSet.room &&
timelineSet.room.getUnfilteredTimelineSet() === timelineSet)
{
EventTimeline.setEventMetadata(event, stateContext, atStart);
// modify state
if (event.isState()) {
stateContext.setStateEvents([event]);
// it is possible that the act of setting the state event means we
// can set more metadata (specifically sender/target props), so try
// it again if the prop wasn't previously set. It may also mean that
// the sender/target is updated (if the event set was a room member event)
// so we want to use the *updated* member (new avatar/name) instead.
//
// However, we do NOT want to do this on member events if we're going
// back in time, else we'll set the .sender value for BEFORE the given
// member event, whereas we want to set the .sender value for the ACTUAL
// member event itself.
if (!event.sender || (event.getType() === "m.room.member" && !atStart)) {
EventTimeline.setEventMetadata(event, stateContext, atStart);
}
}
}
var insertIndex;
if (atStart) {
insertIndex = 0;
} else {
insertIndex = this._events.length;
}
this._events.splice(insertIndex, 0, event); // insert element
if (atStart) {
this._baseIndex++;
}
};
/**
* Static helper method to set sender and target properties
*
* @param {MatrixEvent} event the event whose metadata is to be set
* @param {RoomState} stateContext the room state to be queried
* @param {bool} toStartOfTimeline if true the event's forwardLooking flag is set false
*/
EventTimeline.setEventMetadata = function(event, stateContext, toStartOfTimeline) {
// set sender and target properties
event.sender = stateContext.getSentinelMember(
event.getSender()
);
if (event.getType() === "m.room.member") {
event.target = stateContext.getSentinelMember(
event.getStateKey()
);
}
if (event.isState()) {
// room state has no concept of 'old' or 'current', but we want the
// room state to regress back to previous values if toStartOfTimeline
// is set, which means inspecting prev_content if it exists. This
// is done by toggling the forwardLooking flag.
if (toStartOfTimeline) {
event.forwardLooking = false;
}
}
};
/**
* Remove an event from the timeline
*
* @param {string} eventId ID of event to be removed
* @return {?MatrixEvent} removed event, or null if not found
*/
EventTimeline.prototype.removeEvent = function(eventId) {
for (var i = this._events.length - 1; i >= 0; i--) {
var ev = this._events[i];
if (ev.getId() == eventId) {
this._events.splice(i, 1);
if (i < this._baseIndex) {
this._baseIndex--;
}
return ev;
}
}
return null;
};
/**
* Return a string to identify this timeline, for debugging
*
* @return {string} name for this timeline
*/
EventTimeline.prototype.toString = function() {
return this._name;
};
/**
* The EventTimeline class
*/
module.exports = EventTimeline;
+278 -22
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
@@ -6,6 +21,9 @@
* @module models/event
*/
var EventEmitter = require("events").EventEmitter;
var utils = require('../utils.js');
/**
* Enum for event statuses.
@@ -15,21 +33,33 @@
module.exports.EventStatus = {
/** The event was not sent and will no longer be retried. */
NOT_SENT: "not_sent",
/** The message is being encrypted */
ENCRYPTING: "encrypting",
/** The event is in the process of being sent. */
SENDING: "sending",
/** The event is in a queue waiting to be sent. */
QUEUED: "queued"
QUEUED: "queued",
/** The event has been sent to the server, but we have not yet received the
* echo. */
SENT: "sent",
/** The event was cancelled before it was successfully sent. */
CANCELLED: "cancelled",
};
/**
* Construct a Matrix Event object
* @constructor
*
* @param {Object} event The raw event to be wrapped in this DAO
* @param {boolean} encrypted Was the event encrypted
* @prop {Object} event The raw event. <b>Do not access this property</b>
* directly unless you absolutely have to. Prefer the getter methods defined on
* this class. Using the getter methods shields your app from
* changes to event JSON between Matrix versions.
*
* @prop {Object} event The raw (possibly encrypted) event. <b>Do not access
* this property</b> directly unless you absolutely have to. Prefer the getter
* methods defined on this class. Using the getter methods shields your app
* from changes to event JSON between Matrix versions.
*
* @prop {RoomMember} sender The room member who sent this event, or null e.g.
* this is a presence event.
* @prop {RoomMember} target The room member who is the target of this event, e.g.
@@ -39,15 +69,24 @@ module.exports.EventStatus = {
* that getDirectionalContent() will return event.content and not event.prev_content.
* Default: true. <strong>This property is experimental and may change.</strong>
*/
module.exports.MatrixEvent = function MatrixEvent(event, encrypted) {
module.exports.MatrixEvent = function MatrixEvent(
event
) {
this.event = event || {};
this.sender = null;
this.target = null;
this.status = null;
this.forwardLooking = true;
this.encrypted = Boolean(encrypted);
this._pushActions = null;
this._clearEvent = {};
this._keysProved = {};
this._keysClaimed = {};
};
module.exports.MatrixEvent.prototype = {
utils.inherits(module.exports.MatrixEvent, EventEmitter);
utils.extend(module.exports.MatrixEvent.prototype, {
/**
* Get the event_id for this event.
@@ -63,23 +102,26 @@ module.exports.MatrixEvent.prototype = {
* @return {string} The user ID, e.g. <code>@alice:matrix.org</code>
*/
getSender: function() {
return this.event.user_id;
return this.event.sender || this.event.user_id; // v2 / v1
},
/**
* Get the type of event.
* Get the (decrypted, if necessary) type of event.
*
* @return {string} The event type, e.g. <code>m.room.message</code>
*/
getType: function() {
return this.event.type;
return this._clearEvent.type || this.event.type;
},
/**
* Get the type of the event that will be sent to the homeserver.
* Get the (possibly encrypted) type of the event that will be sent to the
* homeserver.
*
* @return {string} The event type.
*/
getWireType: function() {
return this.encryptedType || this.event.type;
return this.event.type;
},
/**
@@ -101,19 +143,22 @@ module.exports.MatrixEvent.prototype = {
},
/**
* Get the event content JSON.
* Get the (decrypted, if necessary) event content JSON.
*
* @return {Object} The event content JSON, or an empty object.
*/
getContent: function() {
return this.event.content || {};
return this._clearEvent.content || this.event.content || {};
},
/**
* Get the event content JSON that will be sent to the homeserver.
* Get the (possibly encrypted) event content JSON that will be sent to the
* homeserver.
*
* @return {Object} The event content JSON, or an empty object.
*/
getWireContent: function() {
return this.encryptedContent || this.event.content || {};
return this.event.content || {};
},
/**
@@ -122,12 +167,15 @@ module.exports.MatrixEvent.prototype = {
* @return {Object} The previous event content JSON, or an empty object.
*/
getPrevContent: function() {
return this.event.prev_content || {};
// v2 then v1 then default
return this.getUnsigned().prev_content || this.event.prev_content || {};
},
/**
* Get either 'content' or 'prev_content' depending on if this event is
* 'forward-looking' or not. This can be modified via event.forwardLooking.
* In practice, this means we get the chronologically earlier content value
* for this event (this method should surely be called getEarlierContent)
* <strong>This method is experimental and may change.</strong>
* @return {Object} event.content if this event is forward-looking, else
* event.prev_content.
@@ -143,7 +191,7 @@ module.exports.MatrixEvent.prototype = {
* @return {Number} The age of this event in milliseconds.
*/
getAge: function() {
return this.event.age;
return this.getUnsigned().age || this.event.age; // v2 / v1
},
/**
@@ -163,11 +211,219 @@ module.exports.MatrixEvent.prototype = {
return this.event.state_key !== undefined;
},
/**
* Replace the content of this event with encrypted versions.
* (This is used when sending an event; it should not be used by applications).
*
* @internal
*
* @param {string} crypto_type type of the encrypted event - typically
* <tt>"m.room.encrypted"</tt>
*
* @param {object} crypto_content raw 'content' for the encrypted event.
* @param {object} keys The local keys claimed and proved by this event.
*/
makeEncrypted: function(crypto_type, crypto_content, keys) {
// keep the plain-text data for 'view source'
this._clearEvent = {
type: this.event.type,
content: this.event.content,
};
this.event.type = crypto_type;
this.event.content = crypto_content;
this._keysProved = keys;
this._keysClaimed = keys;
},
/**
* Update the cleartext data on this event.
*
* (This is used after decrypting an event; it should not be used by applications).
*
* @internal
*
* @fires module:models/event.MatrixEvent#"Event.decrypted"
*
* @param {Object} clearEvent The plaintext payload for the event
* (typically containing <tt>type</tt> and <tt>content</tt> fields).
*
* @param {Object=} keysProved Keys owned by the sender of this event.
* See {@link module:models/event.MatrixEvent#getKeysProved}.
*
* @param {Object=} keysClaimed Keys the sender of this event claims.
* See {@link module:models/event.MatrixEvent#getKeysClaimed}.
*/
setClearData: function(clearEvent, keysProved, keysClaimed) {
this._clearEvent = clearEvent;
this._keysProved = keysProved || {};
this._keysClaimed = keysClaimed || {};
this.emit("Event.decrypted", this);
},
/**
* Check if the event is encrypted.
* @return {boolean} True if this event is encrypted.
*/
isEncrypted: function() {
return this.encrypted;
}
return this.event.type === "m.room.encrypted";
},
/**
* The curve25519 key that sent this event
* @return {string}
*/
getSenderKey: function() {
return this.getKeysProved().curve25519 || null;
},
/**
* The keys that must have been owned by the sender of this encrypted event.
* <p>
* These don't necessarily have to come from this event itself, but may be
* implied by the cryptographic session.
*
* @return {Object<string, string>}
*/
getKeysProved: function() {
return this._keysProved;
},
/**
* The additional keys the sender of this encrypted event claims to possess.
* <p>
* These don't necessarily have to come from this event itself, but may be
* implied by the cryptographic session.
* For example megolm messages don't claim keys directly, but instead
* inherit a claim from the olm message that established the session.
*
* @return {Object<string, string>}
*/
getKeysClaimed: function() {
return this._keysClaimed;
},
getUnsigned: function() {
return this.event.unsigned || {};
},
/**
* Update the content of an event in the same way it would be by the server
* if it were redacted before it was sent to us
*
* @param {module:models/event.MatrixEvent} redaction_event
* event causing the redaction
*/
makeRedacted: function(redaction_event) {
// quick sanity-check
if (!redaction_event.event) {
throw new Error("invalid redaction_event in makeRedacted");
}
// we attempt to replicate what we would see from the server if
// the event had been redacted before we saw it.
//
// The server removes (most of) the content of the event, and adds a
// "redacted_because" key to the unsigned section containing the
// redacted event.
if (!this.event.unsigned) {
this.event.unsigned = {};
}
this.event.unsigned.redacted_because = redaction_event.event;
var key;
for (key in this.event) {
if (!this.event.hasOwnProperty(key)) { continue; }
if (!_REDACT_KEEP_KEY_MAP[key]) {
delete this.event[key];
}
}
var keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {};
var content = this.getContent();
for (key in content) {
if (!content.hasOwnProperty(key)) { continue; }
if (!keeps[key]) {
delete content[key];
}
}
},
/**
* Check if this event has been redacted
*
* @return {boolean} True if this event has been redacted
*/
isRedacted: function() {
return Boolean(this.getUnsigned().redacted_because);
},
/**
* Get the push actions, if known, for this event
*
* @return {?Object} push actions
*/
getPushActions: function() {
return this._pushActions;
},
/**
* Set the push actions for this event.
*
* @param {Object} pushActions push actions
*/
setPushActions: function(pushActions) {
this._pushActions = pushActions;
},
});
/* http://matrix.org/docs/spec/r0.0.1/client_server.html#redactions says:
*
* the server should strip off any keys not in the following list:
* event_id
* type
* room_id
* user_id
* state_key
* prev_state
* content
* [we keep 'unsigned' as well, since that is created by the local server]
*
* The content object should also be stripped of all keys, unless it is one of
* one of the following event types:
* m.room.member allows key membership
* m.room.create allows key creator
* m.room.join_rules allows key join_rule
* m.room.power_levels allows keys ban, events, events_default, kick,
* redact, state_default, users, users_default.
* m.room.aliases allows key aliases
*/
// a map giving the keys we keep when an event is redacted
var _REDACT_KEEP_KEY_MAP = [
'event_id', 'type', 'room_id', 'user_id', 'state_key', 'prev_state',
'content', 'unsigned',
].reduce(function(ret, val) { ret[val] = 1; return ret; }, {});
// a map from event type to the .content keys we keep when an event is redacted
var _REDACT_KEEP_CONTENT_MAP = {
'm.room.member': {'membership': 1},
'm.room.create': {'creator': 1},
'm.room.join_rules': {'join_rule': 1},
'm.room.power_levels': {'ban': 1, 'events': 1, 'events_default': 1,
'kick': 1, 'redact': 1, 'state_default': 1,
'users': 1, 'users_default': 1,
},
'm.room.aliases': {'aliases': 1},
};
/**
* Fires when an event is decrypted
*
* @event module:models/event.MatrixEvent#"Event.decrypted"
*
* @param {module:models/event.MatrixEvent} event
* The matrix event which has been decrypted
*/
+78 -33
View File
@@ -1,14 +1,33 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/room-member
*/
var EventEmitter = require("events").EventEmitter;
var ContentRepo = require("../content-repo");
var utils = require("../utils");
/**
* Construct a new room member.
*
* @constructor
* @alias module:models/room-member
*
* @param {string} roomId The room ID of the member.
* @param {string} userId The user ID of the member.
* @prop {string} roomId The room ID for this member.
@@ -61,11 +80,11 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) {
this.name = calculateDisplayName(this, event, roomState);
if (oldMembership !== this.membership) {
this._updateModifiedTime();
this.emit("RoomMember.membership", event, this);
this.emit("RoomMember.membership", event, this, oldMembership);
}
if (oldName !== this.name) {
this._updateModifiedTime();
this.emit("RoomMember.name", event, this);
this.emit("RoomMember.name", event, this, oldName);
}
};
@@ -86,11 +105,14 @@ RoomMember.prototype.setPowerLevelEvent = function(powerLevelEvent) {
});
var oldPowerLevel = this.powerLevel;
var oldPowerLevelNorm = this.powerLevelNorm;
this.powerLevel = (
powerLevelEvent.getContent().users[this.userId] ||
powerLevelEvent.getContent().users_default ||
0
);
if (powerLevelEvent.getContent().users[this.userId] !== undefined) {
this.powerLevel = powerLevelEvent.getContent().users[this.userId];
} else if (powerLevelEvent.getContent().users_default !== undefined) {
this.powerLevel = powerLevelEvent.getContent().users_default;
} else {
this.powerLevel = 0;
}
this.powerLevelNorm = 0;
if (maxLevel > 0) {
this.powerLevelNorm = (this.powerLevel * 100) / maxLevel;
@@ -147,25 +169,49 @@ RoomMember.prototype.getLastModifiedTime = function() {
return this._modified;
};
/**
* Get the avatar URL for a room member.
* @param {string} baseUrl The base homeserver URL See
* {@link module:client~MatrixClient#getHomeserverUrl}.
* @param {Number} width The desired width of the thumbnail.
* @param {Number} height The desired height of the thumbnail.
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @param {Boolean} allowDefault (optional) Passing false causes this method to
* return null if the user has no avatar image. Otherwise, a default image URL
* will be returned. Default: true.
* @param {Boolean} allowDirectLinks (optional) If true, the avatar URL will be
* returned even if it is a direct hyperlink rather than a matrix content URL.
* If false, any non-matrix content URLs will be ignored. Setting this option to
* true will expose URLs that, if fetched, will leak information about the user
* to anyone who they share a room with.
* @return {?string} the avatar URL or null.
*/
RoomMember.prototype.getAvatarUrl =
function(baseUrl, width, height, resizeMethod, allowDefault, allowDirectLinks) {
if (allowDefault === undefined) { allowDefault = true; }
if (!this.events.member && !allowDefault) {
return null;
}
var rawUrl = this.events.member ? this.events.member.getContent().avatar_url : null;
var httpUrl = ContentRepo.getHttpUriForMxc(
baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks
);
if (httpUrl) {
return httpUrl;
}
else if (allowDefault) {
return ContentRepo.getIdenticonUri(
baseUrl, this.userId, width, height
);
}
return null;
};
function calculateDisplayName(member, event, roomState) {
var displayName = event.getDirectionalContent().displayname;
var selfUserId = member.userId;
/*
// FIXME: this would be great but still needs to use the
// full userId to disambiguate if needed...
if (!displayName) {
var matches = selfUserId.match(/^@(.*?):/);
if (matches) {
return matches[1];
}
else {
return selfUserId;
}
}
*/
if (!displayName) {
return selfUserId;
}
@@ -174,18 +220,13 @@ function calculateDisplayName(member, event, roomState) {
return displayName;
}
var stateEvents = utils.filter(
roomState.getStateEvents("m.room.member"),
function(e) {
return e.getContent().displayname === displayName &&
e.getSender() !== selfUserId;
}
);
if (stateEvents.length > 0) {
// need to disambiguate
var userIds = roomState.getUserIdsWithDisplayName(displayName);
var otherUsers = userIds.filter(function(u) {
return u !== selfUserId;
});
if (otherUsers.length > 0) {
return displayName + " (" + selfUserId + ")";
}
return displayName;
}
@@ -199,6 +240,8 @@ module.exports = RoomMember;
* @event module:client~MatrixClient#"RoomMember.name"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomMember} member The member whose RoomMember.name changed.
* @param {string?} oldName The previous name. Null if the member didn't have a
* name previously.
* @example
* matrixClient.on("RoomMember.name", function(event, member){
* var newName = member.name;
@@ -210,8 +253,10 @@ module.exports = RoomMember;
* @event module:client~MatrixClient#"RoomMember.membership"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomMember} member The member whose RoomMember.membership changed.
* @param {string?} oldMembership The previous membership state. Null if it's a
* new member.
* @example
* matrixClient.on("RoomMember.membership", function(event, member){
* matrixClient.on("RoomMember.membership", function(event, member, oldMembership){
* var newState = member.membership;
* });
*/
+208 -1
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/room-state
@@ -10,7 +25,8 @@ var RoomMember = require("./room-member");
/**
* Construct room state.
* @constructor
* @param {string} roomId Required. The ID of the room which has this state.
* @param {?string} roomId Optional. The ID of the room which has this state.
* If none is specified it just tracks paginationTokens, useful for notifTimelineSet
* @prop {Object.<string, RoomMember>} members The room member dictionary, keyed
* on the user's ID.
* @prop {Object.<string, Object.<string, MatrixEvent>>} events The state
@@ -31,6 +47,9 @@ function RoomState(roomId) {
// userId: RoomMember
};
this._updateModifiedTime();
this._displayNameToUserIds = {};
this._userIdsToDisplayNames = {};
this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite
}
utils.inherits(RoomState, EventEmitter);
@@ -108,6 +127,12 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
self.events[event.getType()] = {};
}
self.events[event.getType()][event.getStateKey()] = event;
if (event.getType() === "m.room.member") {
_updateDisplayNameCache(
self, event.getStateKey(), event.getContent().displayname
);
_updateThirdPartyTokenCache(self, event);
}
self.emit("RoomState.events", event, self);
});
@@ -121,6 +146,21 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
if (event.getType() === "m.room.member") {
var userId = event.getStateKey();
// leave events apparently elide the displayname or avatar_url,
// so let's fake one up so that we don't leak user ids
// into the timeline
if (event.getContent().membership === "leave" ||
event.getContent().membership === "ban")
{
event.getContent().avatar_url =
event.getContent().avatar_url ||
event.getPrevContent().avatar_url;
event.getContent().displayname =
event.getContent().displayname ||
event.getPrevContent().displayname;
}
var member = self.members[userId];
if (!member) {
member = new RoomMember(event.getRoomId(), userId);
@@ -149,6 +189,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
var members = utils.values(self.members);
utils.forEach(members, function(member) {
member.setPowerLevelEvent(event);
self.emit("RoomState.members", event, self, member);
});
}
});
@@ -164,6 +205,16 @@ RoomState.prototype.setTypingEvent = function(event) {
});
};
/**
* Get the m.room.member event which has the given third party invite token.
*
* @param {string} token The token
* @return {?MatrixEvent} The m.room.member event or null
*/
RoomState.prototype.getInviteForThreePidToken = function(token) {
return this._tokenToInvite[token] || null;
};
/**
* Update the last modified time to the current time.
*/
@@ -180,11 +231,167 @@ RoomState.prototype.getLastModifiedTime = function() {
return this._modified;
};
/**
* Get user IDs with the specified display name.
* @param {string} displayName The display name to get user IDs from.
* @return {string[]} An array of user IDs or an empty array.
*/
RoomState.prototype.getUserIdsWithDisplayName = function(displayName) {
return this._displayNameToUserIds[displayName] || [];
};
/**
* Short-form for maySendEvent('m.room.message', userId)
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* message events into the given room.
*/
RoomState.prototype.maySendMessage = function(userId) {
return this._maySendEventOfType('m.room.message', userId, false);
};
/**
* Returns true if the given user ID has permission to send a normal
* event of type `eventType` into this room.
* @param {string} type The type of event to test
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* the given type of event into this room,
* according to the room's state.
*/
RoomState.prototype.maySendEvent = function(eventType, userId) {
return this._maySendEventOfType(eventType, userId, false);
};
/**
* Returns true if the given MatrixClient has permission to send a state
* event of type `stateEventType` into this room.
* @param {string} type The type of state events to test
* @param {MatrixClient} The client to test permission for
* @return {boolean} true if the given client should be permitted to send
* the given type of state event into this room,
* according to the room's state.
*/
RoomState.prototype.mayClientSendStateEvent = function(stateEventType, cli) {
if (cli.isGuest()) {
return false;
}
return this.maySendStateEvent(stateEventType, cli.credentials.userId);
};
/**
* Returns true if the given user ID has permission to send a state
* event of type `stateEventType` into this room.
* @param {string} type The type of state events to test
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* the given type of state event into this room,
* according to the room's state.
*/
RoomState.prototype.maySendStateEvent = function(stateEventType, userId) {
return this._maySendEventOfType(stateEventType, userId, true);
};
/**
* Returns true if the given user ID has permission to send a normal or state
* event of type `eventType` into this room.
* @param {string} type The type of event to test
* @param {string} userId The user ID of the user to test permission for
* @param {boolean} state If true, tests if the user may send a state
event of this type. Otherwise tests whether
they may send a regular event.
* @return {boolean} true if the given user ID should be permitted to send
* the given type of event into this room,
* according to the room's state.
*/
RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
var member = this.getMember(userId);
if (!member || member.membership == 'leave') { return false; }
var power_levels_event = this.getStateEvents('m.room.power_levels', '');
var power_levels;
var events_levels = {};
var default_user_level = 0;
var user_levels = [];
var state_default = 0;
var events_default = 0;
if (power_levels_event) {
power_levels = power_levels_event.getContent();
events_levels = power_levels.events || {};
default_user_level = parseInt(power_levels.users_default || 0);
user_levels = power_levels.users || {};
if (power_levels.state_default !== undefined) {
state_default = power_levels.state_default;
} else {
state_default = 50;
}
if (power_levels.events_default !== undefined) {
events_default = power_levels.events_default;
}
}
var required_level = state ? state_default : events_default;
if (events_levels[eventType] !== undefined) {
required_level = events_levels[eventType];
}
return member.powerLevel >= required_level;
};
/**
* The RoomState class.
*/
module.exports = RoomState;
function _updateThirdPartyTokenCache(roomState, memberEvent) {
if (!memberEvent.getContent().third_party_invite) {
return;
}
var token = (memberEvent.getContent().third_party_invite.signed || {}).token;
if (!token) {
return;
}
var threePidInvite = roomState.getStateEvents(
"m.room.third_party_invite", token
);
if (!threePidInvite) {
return;
}
roomState._tokenToInvite[token] = memberEvent;
}
function _updateDisplayNameCache(roomState, userId, displayName) {
var oldName = roomState._userIdsToDisplayNames[userId];
delete roomState._userIdsToDisplayNames[userId];
if (oldName) {
// Remove the old name from the cache.
// We clobber the user_id > name lookup but the name -> [user_id] lookup
// means we need to remove that user ID from that array rather than nuking
// the lot.
var existingUserIds = roomState._displayNameToUserIds[oldName] || [];
for (var i = 0; i < existingUserIds.length; i++) {
if (existingUserIds[i] === userId) {
// remove this user ID from this array
existingUserIds.splice(i, 1);
i--;
}
}
roomState._displayNameToUserIds[oldName] = existingUserIds;
}
roomState._userIdsToDisplayNames[userId] = displayName;
if (!roomState._displayNameToUserIds[displayName]) {
roomState._displayNameToUserIds[displayName] = [];
}
roomState._displayNameToUserIds[displayName].push(userId);
}
/**
* Fires whenever the event dictionary in room state is updated.
* @event module:client~MatrixClient#"RoomState.events"
+15
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/room-summary
+1188 -175
View File
File diff suppressed because it is too large Load Diff
+66
View File
@@ -0,0 +1,66 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/search-result
*/
var EventContext = require("./event-context");
var utils = require("../utils");
/**
* Construct a new SearchResult
*
* @param {number} rank where this SearchResult ranks in the results
* @param {event-context.EventContext} eventContext the matching event and its
* context
*
* @constructor
*/
function SearchResult(rank, eventContext) {
this.rank = rank;
this.context = eventContext;
}
/**
* Create a SearchResponse from the response to /search
* @static
* @param {Object} jsonObj
* @param {function} eventMapper
* @return {SearchResult}
*/
SearchResult.fromJson = function(jsonObj, eventMapper) {
var jsonContext = jsonObj.context || {};
var events_before = jsonContext.events_before || [];
var events_after = jsonContext.events_after || [];
var context = new EventContext(eventMapper(jsonObj.result));
context.setPaginateToken(jsonContext.start, true);
context.addEvents(utils.map(events_before, eventMapper), true);
context.addEvents(utils.map(events_after, eventMapper), false);
context.setPaginateToken(jsonContext.end, false);
return new SearchResult(jsonObj.rank, context);
};
/**
* The SearchResult class
*/
module.exports = SearchResult;
+122 -8
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/user
@@ -15,16 +30,28 @@
* @prop {string} displayName The 'displayname' of the user if known.
* @prop {string} avatarUrl The 'avatar_url' of the user if known.
* @prop {string} presence The presence enum if known.
* @prop {Number} lastActiveAgo The last time the user performed some action in ms.
* @prop {string} presenceStatusMsg The presence status message if known.
* @prop {Number} lastActiveAgo The time elapsed in ms since the user interacted
* proactively with the server, or we saw a message from the user
* @prop {Number} lastPresenceTs Timestamp (ms since the epoch) for when we last
* received presence data for this user. We can subtract
* lastActiveAgo from this to approximate an absolute value for
* when a user was last active.
* @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be
* an approximation and that the user should be seen as active 'now'
* @prop {Object} events The events describing this user.
* @prop {MatrixEvent} events.presence The m.presence event for this user.
*/
function User(userId) {
this.userId = userId;
this.presence = "offline";
this.presenceStatusMsg = null;
this.displayName = userId;
this.rawDisplayName = userId;
this.avatarUrl = null;
this.lastActiveAgo = 0;
this.lastPresenceTs = 0;
this.currentlyActive = false;
this.events = {
presence: null,
profile: null
@@ -53,27 +80,82 @@ User.prototype.setPresenceEvent = function(event) {
if (event.getContent().presence !== this.presence || firstFire) {
eventsToFire.push("User.presence");
}
if (event.getContent().avatar_url !== this.avatarUrl) {
if (event.getContent().avatar_url &&
event.getContent().avatar_url !== this.avatarUrl)
{
eventsToFire.push("User.avatarUrl");
}
if (event.getContent().displayname !== this.displayName) {
if (event.getContent().displayname &&
event.getContent().displayname !== this.displayName)
{
eventsToFire.push("User.displayName");
}
if (event.getContent().currently_active !== undefined &&
event.getContent().currently_active !== this.currentlyActive)
{
eventsToFire.push("User.currentlyActive");
}
this.presence = event.getContent().presence;
this.displayName = event.getContent().displayname;
this.avatarUrl = event.getContent().avatar_url;
this.lastActiveAgo = event.getContent().last_active_ago;
eventsToFire.push("User.lastPresenceTs");
if (eventsToFire.length > 0) {
this._updateModifiedTime();
if (event.getContent().status_msg) {
this.presenceStatusMsg = event.getContent().status_msg;
}
if (event.getContent().displayname) {
this.displayName = event.getContent().displayname;
}
if (event.getContent().avatar_url) {
this.avatarUrl = event.getContent().avatar_url;
}
this.lastActiveAgo = event.getContent().last_active_ago;
this.lastPresenceTs = Date.now();
this.currentlyActive = event.getContent().currently_active;
this._updateModifiedTime();
for (var i = 0; i < eventsToFire.length; i++) {
this.emit(eventsToFire[i], event, this);
}
};
/**
* Manually set this user's display name. No event is emitted in response to this
* as there is no underlying MatrixEvent to emit with.
* @param {string} name The new display name.
*/
User.prototype.setDisplayName = function(name) {
var oldName = this.displayName;
this.displayName = name;
if (name !== oldName) {
this._updateModifiedTime();
}
};
/**
* Manually set this user's non-disambiguated display name. No event is emitted
* in response to this as there is no underlying MatrixEvent to emit with.
* @param {string} name The new display name.
*/
User.prototype.setRawDisplayName = function(name) {
this.rawDisplayName = name;
};
/**
* Manually set this user's avatar URL. No event is emitted in response to this
* as there is no underlying MatrixEvent to emit with.
* @param {string} url The new avatar URL.
*/
User.prototype.setAvatarUrl = function(url) {
var oldUrl = this.avatarUrl;
this.avatarUrl = url;
if (url !== oldUrl) {
this._updateModifiedTime();
}
};
/**
* Update the last modified time to the current time.
*/
@@ -91,11 +173,32 @@ User.prototype.getLastModifiedTime = function() {
return this._modified;
};
/**
* Get the absolute timestamp when this User was last known active on the server.
* It is *NOT* accurate if this.currentlyActive is true.
* @return {number} The timestamp
*/
User.prototype.getLastActiveTs = function() {
return this.lastPresenceTs - this.lastActiveAgo;
};
/**
* The User class.
*/
module.exports = User;
/**
* Fires whenever any user's lastPresenceTs changes,
* ie. whenever any presence event is received for a user.
* @event module:client~MatrixClient#"User.lastPresenceTs"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {User} user The user whose User.lastPresenceTs changed.
* @example
* matrixClient.on("User.lastPresenceTs", function(event, user){
* var newlastPresenceTs = user.lastPresenceTs;
* });
*/
/**
* Fires whenever any user's presence changes.
* @event module:client~MatrixClient#"User.presence"
@@ -107,6 +210,17 @@ module.exports = User;
* });
*/
/**
* Fires whenever any user's currentlyActive changes.
* @event module:client~MatrixClient#"User.currentlyActive"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {User} user The user whose User.currentlyActive changed.
* @example
* matrixClient.on("User.currentlyActive", function(event, user){
* var newCurrentlyActive = user.currentlyActive;
* });
*/
/**
* Fires whenever any user's display name changes.
* @event module:client~MatrixClient#"User.displayName"
+75 -25
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module pushprocessor
*/
@@ -107,10 +122,12 @@ function PushProcessor(client) {
var eventFulfillsRoomMemberCountCondition = function(cond, ev) {
if (!cond.is) { return false; }
var room = client.getRoom(ev.room_id);
var room = client.getRoom(ev.getRoomId());
if (!room || !room.currentState || !room.currentState.members) { return false; }
var memberCount = Object.keys(room.currentState.members).length;
var memberCount = Object.keys(room.currentState.members).filter(function(m) {
return room.currentState.members[m].membership == 'join';
}).length;
var m = cond.is.match(/^([=<>]*)([0-9]*)$/);
if (!m) { return false; }
@@ -135,18 +152,21 @@ function PushProcessor(client) {
};
var eventFulfillsDisplayNameCondition = function(cond, ev) {
if (!ev.content || ! ev.content.body || typeof ev.content.body != 'string') {
var content = ev.getContent();
if (!content || !content.body || typeof content.body != 'string') {
return false;
}
var room = client.getRoom(ev.room_id);
var room = client.getRoom(ev.getRoomId());
if (!room || !room.currentState || !room.currentState.members ||
!room.currentState.getMember(client.credentials.userId)) { return false; }
var displayName = room.currentState.getMember(client.credentials.userId).name;
var pat = new RegExp("\\b" + escapeRegExp(displayName) + "\\b", 'i');
return ev.content.body.search(pat) > -1;
// N.B. we can't use \b as it chokes on unicode. however \W seems to be okay
// as shorthand for [^0-9A-Za-z_].
var pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i');
return content.body.search(pat) > -1;
};
var eventFulfillsDeviceCondition = function(cond, ev) {
@@ -159,7 +179,7 @@ function PushProcessor(client) {
var pat;
if (cond.key == 'content.body') {
pat = '\\b' + globToRegexp(cond.pattern) + '\\b';
pat = '(^|\\W)' + globToRegexp(cond.pattern) + '(\\W|$)';
} else {
pat = '^' + globToRegexp(cond.pattern) + '$';
}
@@ -185,7 +205,21 @@ function PushProcessor(client) {
var valueForDottedKey = function(key, ev) {
var parts = key.split('.');
var val = ev;
var val;
// special-case the first component to deal with encrypted messages
var firstPart = parts[0];
if (firstPart == 'content') {
val = ev.getContent();
parts.shift();
} else if (firstPart == 'type') {
val = ev.getType();
parts.shift();
} else {
// use the raw event for any other fields
val = ev.event;
}
while (parts.length > 0) {
var thispart = parts.shift();
if (!val[thispart]) { return null; }
@@ -195,8 +229,8 @@ function PushProcessor(client) {
};
var matchingRuleForEventWithRulesets = function(ev, rulesets) {
if (!rulesets) { return null; }
if (ev.user_id == client.credentials.userId) { return null; }
if (!rulesets || !rulesets.device) { return null; }
if (ev.getSender() == client.credentials.userId) { return null; }
var allDevNames = Object.keys(rulesets.device);
for (var i = 0; i < allDevNames.length; ++i) {
@@ -209,25 +243,11 @@ function PushProcessor(client) {
return matchingRuleFromKindSet(ev, rulesets.global);
};
var actionListToActionsObject = function(actionlist) {
var actionobj = { 'notify': false, 'tweaks': {} };
for (var i = 0; i < actionlist.length; ++i) {
var action = actionlist[i];
if (action === 'notify') {
actionobj.notify = true;
} else if (typeof action === 'object') {
if (action.value === undefined) { action.value = true; }
actionobj.tweaks[action.set_tweak] = action.value;
}
}
return actionobj;
};
var pushActionsForEventAndRulesets = function(ev, rulesets) {
var rule = matchingRuleForEventWithRulesets(ev, rulesets);
if (!rule) { return {}; }
var actionObj = actionListToActionsObject(rule.actions);
var actionObj = PushProcessor.actionListToActionsObject(rule.actions);
// Some actions are implicit in some situations: we add those here
if (actionObj.tweaks.highlight === undefined) {
@@ -239,11 +259,40 @@ function PushProcessor(client) {
return actionObj;
};
/**
* Get the user's push actions for the given event
*
* @param {module:models/event.MatrixEvent} ev
*
* @return {PushAction}
*/
this.actionsForEvent = function(ev) {
return pushActionsForEventAndRulesets(ev, client.pushRules);
};
}
/**
* Convert a list of actions into a object with the actions as keys and their values
* eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ]
* becomes { notify: true, tweaks: { sound: 'default' } }
* @param {array} actionlist The actions list
*
* @return {object} A object with key 'notify' (true or false) and an object of actions
*/
PushProcessor.actionListToActionsObject = function(actionlist) {
var actionobj = { 'notify': false, 'tweaks': {} };
for (var i = 0; i < actionlist.length; ++i) {
var action = actionlist[i];
if (action === 'notify') {
actionobj.notify = true;
} else if (typeof action === 'object') {
if (action.value === undefined) { action.value = true; }
actionobj.tweaks[action.set_tweak] = action.value;
}
}
return actionobj;
};
/**
* @typedef {Object} PushAction
* @type {Object}
@@ -257,3 +306,4 @@ function PushProcessor(client) {
/** The PushProcessor class. */
module.exports = PushProcessor;
+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;
}
+21
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module which manages queuing, scheduling and retrying
@@ -133,6 +148,12 @@ MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
// client error; no amount of retrying with save you now.
return -1;
}
// we ship with browser-request which returns { cors: rejected } when trying
// with no connection, so if we match that, give up since they have no conn.
if (err.cors === "rejected") {
return -1;
}
if (err.name === "M_LIMIT_EXCEEDED") {
var waitTime = err.data.retry_after_ms;
if (waitTime) {
+167 -2
View File
@@ -1,15 +1,36 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module. See {@link MatrixInMemoryStore} for the public class.
* @module store/memory
*/
var utils = require("../utils");
var User = require("../models/user");
/**
* Construct a new in-memory data store for the Matrix Client.
* @constructor
* @param {Object=} opts Config options
* @param {LocalStorage} opts.localStorage The local storage instance to persist
* some forms of data such as tokens. Rooms will NOT be stored. See
* {@link WebStorageStore} to persist rooms.
*/
module.exports.MatrixInMemoryStore = function MatrixInMemoryStore() {
module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
opts = opts || {};
this.rooms = {
// roomId: Room
};
@@ -17,6 +38,15 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore() {
// userId: User
};
this.syncToken = null;
this.filters = {
// userId: {
// filterId: Filter
// }
};
this.accountData = {
// type : content
};
this.localStorage = opts.localStorage;
};
module.exports.MatrixInMemoryStore.prototype = {
@@ -29,6 +59,7 @@ module.exports.MatrixInMemoryStore.prototype = {
return this.syncToken;
},
/**
* Set the token to stream from.
* @param {string} token The token to stream from.
@@ -43,6 +74,43 @@ module.exports.MatrixInMemoryStore.prototype = {
*/
storeRoom: function(room) {
this.rooms[room.roomId] = room;
// add listeners for room member changes so we can keep the room member
// map up-to-date.
room.currentState.on("RoomState.members", this._onRoomMember.bind(this));
// add existing members
var self = this;
room.currentState.getMembers().forEach(function(m) {
self._onRoomMember(null, room.currentState, m);
});
},
/**
* Called when a room member in a room being tracked by this store has been
* updated.
* @param {MatrixEvent} event
* @param {RoomState} state
* @param {RoomMember} member
*/
_onRoomMember: function(event, state, member) {
if (member.membership === "invite") {
// We do NOT add invited members because people love to typo user IDs
// which would then show up in these lists (!)
return;
}
var user = this.users[member.userId] || new User(member.userId);
if (member.name) {
user.setDisplayName(member.name);
if (member.events.member) {
user.setRawDisplayName(
member.events.member.getDirectionalContent().displayname
);
}
}
if (member.events.member && member.events.member.getContent().avatar_url) {
user.setAvatarUrl(member.events.member.getContent().avatar_url);
}
this.users[user.userId] = user;
},
/**
@@ -62,6 +130,17 @@ module.exports.MatrixInMemoryStore.prototype = {
return utils.values(this.rooms);
},
/**
* Permanently delete a room.
* @param {string} roomId
*/
removeRoom: function(roomId) {
if (this.rooms[roomId]) {
this.rooms[roomId].removeListener("RoomState.members", this._onRoomMember);
}
delete this.rooms[roomId];
},
/**
* Retrieve a summary of all the rooms.
* @return {RoomSummary[]} A summary of each room.
@@ -89,6 +168,14 @@ module.exports.MatrixInMemoryStore.prototype = {
return this.users[userId] || null;
},
/**
* Retrieve all known users.
* @return {User[]} A list of users, which may be empty.
*/
getUsers: function() {
return utils.values(this.users);
},
/**
* Retrieve scrollback for this room.
* @param {Room} room The matrix room
@@ -109,7 +196,85 @@ module.exports.MatrixInMemoryStore.prototype = {
*/
storeEvents: function(room, events, token, toStart) {
// no-op because they've already been added to the room instance.
}
},
/**
* Store a filter.
* @param {Filter} filter
*/
storeFilter: function(filter) {
if (!filter) { return; }
if (!this.filters[filter.userId]) {
this.filters[filter.userId] = {};
}
this.filters[filter.userId][filter.filterId] = filter;
},
/**
* Retrieve a filter.
* @param {string} userId
* @param {string} filterId
* @return {?Filter} A filter or null.
*/
getFilter: function(userId, filterId) {
if (!this.filters[userId] || !this.filters[userId][filterId]) {
return null;
}
return this.filters[userId][filterId];
},
/**
* Retrieve a filter ID with the given name.
* @param {string} filterName The filter name.
* @return {?string} The filter ID or null.
*/
getFilterIdByName: function(filterName) {
if (!this.localStorage) {
return null;
}
try {
return this.localStorage.getItem("mxjssdk_memory_filter_" + filterName);
}
catch (e) {}
return null;
},
/**
* Set a filter name to ID mapping.
* @param {string} filterName
* @param {string} filterId
*/
setFilterIdByName: function(filterName, filterId) {
if (!this.localStorage) {
return;
}
try {
this.localStorage.setItem("mxjssdk_memory_filter_" + filterName, filterId);
}
catch (e) {}
},
/**
* Store user-scoped account data events.
* N.B. that account data only allows a single event per type, so multiple
* events with the same type will replace each other.
* @param {Array<MatrixEvent>} events The events to store.
*/
storeAccountDataEvents: function(events) {
var self = this;
events.forEach(function(event) {
self.accountData[event.getType()] = event;
});
},
/**
* Get account data event by event type
* @param {string} eventType The event type being queried
* @return {?MatrixEvent} the user account_data event of given type, if any
*/
getAccountData: function(eventType) {
return this.accountData[eventType];
},
// TODO
//setMaxHistoryPerRoom: function(maxHistory) {},
+46
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
@@ -47,6 +62,22 @@ WebStorageSessionStore.prototype = {
return this.store.getItem(KEY_END_TO_END_ACCOUNT);
},
/**
* Store a flag indicating that we have announced the new device.
*/
setDeviceAnnounced: function() {
this.store.setItem(KEY_END_TO_END_ANNOUNCED, "true");
},
/**
* Check if the "device announced" flag is set
*
* @return {boolean} true if the "device announced" flag has been set.
*/
getDeviceAnnounced: function() {
return this.store.getItem(KEY_END_TO_END_ANNOUNCED) == "true";
},
/**
* Stores the known devices for a user.
* @param {string} userId The user's ID.
@@ -89,6 +120,16 @@ WebStorageSessionStore.prototype = {
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
},
getEndToEndInboundGroupSession: function(senderKey, sessionId) {
var key = keyEndToEndInboundGroupSession(senderKey, sessionId);
return this.store.getItem(key);
},
storeEndToEndInboundGroupSession: function(senderKey, sessionId, pickledSession) {
var key = keyEndToEndInboundGroupSession(senderKey, sessionId);
return this.store.setItem(key, pickledSession);
},
/**
* Store the end-to-end state for a room.
* @param {string} roomId The room's ID.
@@ -109,6 +150,7 @@ WebStorageSessionStore.prototype = {
};
var KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
var KEY_END_TO_END_ANNOUNCED = E2E_PREFIX + "announced";
function keyEndToEndDevicesForUser(userId) {
return E2E_PREFIX + "devices/" + userId;
@@ -118,6 +160,10 @@ function keyEndToEndSessions(deviceKey) {
return E2E_PREFIX + "sessions/" + deviceKey;
}
function keyEndToEndInboundGroupSession(senderKey, sessionId) {
return E2E_PREFIX + "inboundgroupsessions/" + senderKey + "/" + sessionId;
}
function keyEndToEndRoom(roomId) {
return E2E_PREFIX + "rooms/" + roomId;
}
+83 -1
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module.
@@ -54,6 +69,14 @@ StubStore.prototype = {
return [];
},
/**
* Permanently delete a room.
* @param {string} roomId
*/
removeRoom: function(roomId) {
return;
},
/**
* No-op.
* @return {Array} An empty array.
@@ -78,6 +101,14 @@ StubStore.prototype = {
return null;
},
/**
* No-op.
* @return {User[]}
*/
getUsers: function() {
return [];
},
/**
* No-op.
* @param {Room} room
@@ -96,7 +127,58 @@ StubStore.prototype = {
* @param {boolean} toStart True if these are paginated results.
*/
storeEvents: function(room, events, token, toStart) {
}
},
/**
* Store a filter.
* @param {Filter} filter
*/
storeFilter: function(filter) {
},
/**
* Retrieve a filter.
* @param {string} userId
* @param {string} filterId
* @return {?Filter} A filter or null.
*/
getFilter: function(userId, filterId) {
return null;
},
/**
* Retrieve a filter ID with the given name.
* @param {string} filterName The filter name.
* @return {?string} The filter ID or null.
*/
getFilterIdByName: function(filterName) {
return null;
},
/**
* Set a filter name to ID mapping.
* @param {string} filterName
* @param {string} filterId
*/
setFilterIdByName: function(filterName, filterId) {
},
/**
* Store user-scoped account data events
* @param {Array<MatrixEvent>} events The events to store.
*/
storeAccountDataEvents: function(events) {
},
/**
* Get account data event by event type
* @param {string} eventType The event type being queried
*/
getAccountData: function(eventType) {
},
// TODO
//setMaxHistoryPerRoom: function(maxHistory) {},
+38 -3
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module. Implementation details:
@@ -358,7 +373,7 @@ WebStorageStore.prototype.scrollback = function(room, limit) {
);
room.addEventsToTimeline(utils.map(scrollback, function(e) {
return new MatrixEvent(e);
}), true);
}), true, room.getLiveTimeline());
this._tokens[room.storageToken] = {
earliestIndex: earliestIndex
@@ -458,6 +473,24 @@ WebStorageStore.prototype._syncTimeline = function(roomId, timelineIndices) {
setItem(this.store, keyName(roomId, "timeline", "live"), []);
};
/**
* Store a filter.
* @param {Filter} filter
*/
WebStorageStore.prototype.storeFilter = function(filter) {
};
/**
* Retrieve a filter.
* @param {string} userId
* @param {string} filterId
* @return {?Filter} A filter or null.
*/
WebStorageStore.prototype.getFilter = function(userId, filterId) {
return null;
};
function SerialisedRoom(roomId) {
this.state = {
events: {}
@@ -514,7 +547,9 @@ SerialisedRoom.fromRoom = function(room, batchSize) {
};
function loadRoom(store, roomId, numEvents, tokenArray) {
var room = new Room(roomId, tokenArray.length);
var room = new Room(roomId, {
storageToken: tokenArray.length
});
// populate state (flatten nested struct to event array)
var currentStateMap = getItem(store, keyName(roomId, "state"));
@@ -559,7 +594,7 @@ function loadRoom(store, roomId, numEvents, tokenArray) {
index--;
}
// add events backwards to diverge old state correctly.
room.addEventsToTimeline(recentEvents.reverse(), true);
room.addEventsToTimeline(recentEvents.reverse(), true, room.getLiveTimeline());
room.oldState.paginationToken = currentStateMap.pagination_token;
// set the token data to let us know which index this room instance is at
// for scrollback.
+1138
View File
File diff suppressed because it is too large Load Diff
+483
View File
@@ -0,0 +1,483 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/** @module timeline-window */
var q = require("q");
var EventTimeline = require("./models/event-timeline");
/**
* @private
*/
var DEBUG = false;
/**
* @private
*/
var debuglog = DEBUG ? console.log.bind(console) : function() {};
/**
* the number of times we ask the server for more events before giving up
*
* @private
*/
var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
/**
* Construct a TimelineWindow.
*
* <p>This abstracts the separate timelines in a Matrix {@link
* module:models/room|Room} into a single iterable thing. It keeps track of
* the start and endpoints of the window, which can be advanced with the help
* of pagination requests.
*
* <p>Before the window is useful, it must be initialised by calling {@link
* module:timeline-window~TimelineWindow#load|load}.
*
* <p>Note that the window will not automatically extend itself when new events
* are received from /sync; you should arrange to call {@link
* module:timeline-window~TimelineWindow#paginate|paginate} on {@link
* module:client~MatrixClient.event:"Room.timeline"|Room.timeline} events.
*
* @param {MatrixClient} client MatrixClient to be used for context/pagination
* requests.
*
* @param {EventTimelineSet} timelineSet The timelineSet to track
*
* @param {Object} [opts] Configuration options for this window
*
* @param {number} [opts.windowLimit = 1000] maximum number of events to keep
* in the window. If more events are retrieved via pagination requests,
* excess events will be dropped from the other end of the window.
*
* @constructor
*/
function TimelineWindow(client, timelineSet, opts) {
opts = opts || {};
this._client = client;
this._timelineSet = timelineSet;
// these will be TimelineIndex objects; they delineate the 'start' and
// 'end' of the window.
//
// _start.index is inclusive; _end.index is exclusive.
this._start = null;
this._end = null;
this._eventCount = 0;
this._windowLimit = opts.windowLimit || 1000;
}
/**
* Initialise the window to point at a given event, or the live timeline
*
* @param {string} [initialEventId] If given, the window will contain the
* given event
* @param {number} [initialWindowSize = 20] Size of the initial window
*
* @return {module:client.Promise}
*/
TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
var self = this;
initialWindowSize = initialWindowSize || 20;
// given an EventTimeline, and an event index within it, initialise our
// fields so that the event in question is in the middle of the window.
var initFields = function(timeline, eventIndex) {
var endIndex = Math.min(timeline.getEvents().length,
eventIndex + Math.ceil(initialWindowSize / 2));
var startIndex = Math.max(0, endIndex - initialWindowSize);
self._start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex());
self._end = new TimelineIndex(timeline, endIndex - timeline.getBaseIndex());
self._eventCount = endIndex - startIndex;
};
// We avoid delaying the resolution of the promise by a reactor tick if
// we already have the data we need, which is important to keep room-switching
// feeling snappy.
//
// TODO: ideally we'd spot getEventTimeline returning a resolved promise and
// skip straight to the find-event loop.
if (initialEventId) {
return this._client.getEventTimeline(this._timelineSet, initialEventId)
.then(function(tl) {
// make sure that our window includes the event
for (var i = 0; i < tl.getEvents().length; i++) {
if (tl.getEvents()[i].getId() == initialEventId) {
initFields(tl, i);
return;
}
}
throw new Error("getEventTimeline result didn't include requested event");
});
} else {
// start with the most recent events
var tl = this._timelineSet.getLiveTimeline();
initFields(tl, tl.getEvents().length);
return q();
}
};
/**
* Check if this window can be extended
*
* <p>This returns true if we either have more events, or if we have a
* pagination token which means we can paginate in that direction. It does not
* necessarily mean that there are more events available in that direction at
* this time.
*
* @param {string} direction EventTimeline.BACKWARDS to check if we can
* paginate backwards; EventTimeline.FORWARDS to check if we can go forwards
*
* @return {boolean} true if we can paginate in the given direction
*/
TimelineWindow.prototype.canPaginate = function(direction) {
var tl;
if (direction == EventTimeline.BACKWARDS) {
tl = this._start;
} else if (direction == EventTimeline.FORWARDS) {
tl = this._end;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
if (!tl) {
debuglog("TimelineWindow: no timeline yet");
return false;
}
if (direction == EventTimeline.BACKWARDS) {
if (tl.index > tl.minIndex()) { return true; }
} else {
if (tl.index < tl.maxIndex()) { return true; }
}
return Boolean(tl.timeline.getNeighbouringTimeline(direction) ||
tl.timeline.getPaginationToken(direction));
};
/**
* Attempt to extend the window
*
* @param {string} direction EventTimeline.BACKWARDS to extend the window
* backwards (towards older events); EventTimeline.FORWARDS to go forwards.
*
* @param {number} size number of events to try to extend by. If fewer than this
* number are immediately available, then we return immediately rather than
* making an API call.
*
* @param {boolean} [makeRequest = true] whether we should make API calls to
* fetch further events if we don't have any at all. (This has no effect if
* the room already knows about additional events in the relevant direction,
* even if there are fewer than 'size' of them, as we will just return those
* we already know about.)
*
* @param {number} [requestLimit = 5] limit for the number of API requests we
* should make.
*
* @return {module:client.Promise} Resolves to a boolean which is true if more events
* were successfully retrieved.
*/
TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
requestLimit) {
// Either wind back the message cap (if there are enough events in the
// timeline to do so), or fire off a pagination request.
if (makeRequest === undefined) {
makeRequest = true;
}
if (requestLimit === undefined) {
requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT;
}
var tl;
if (direction == EventTimeline.BACKWARDS) {
tl = this._start;
} else if (direction == EventTimeline.FORWARDS) {
tl = this._end;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
if (!tl) {
debuglog("TimelineWindow: no timeline yet");
return q(false);
}
if (tl.pendingPaginate) {
return tl.pendingPaginate;
}
// try moving the cap
var count = (direction == EventTimeline.BACKWARDS) ?
tl.retreat(size) : tl.advance(size);
if (count) {
this._eventCount += count;
debuglog("TimelineWindow: increased cap by " + count +
" (now " + this._eventCount + ")");
// remove some events from the other end, if necessary
var excess = this._eventCount - this._windowLimit;
if (excess > 0) {
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
}
return q(true);
}
if (!makeRequest || requestLimit === 0) {
// todo: should we return something different to indicate that there
// might be more events out there, but we haven't found them yet?
return q(false);
}
// try making a pagination request
var token = tl.timeline.getPaginationToken(direction);
if (!token) {
debuglog("TimelineWindow: no token");
return q(false);
}
debuglog("TimelineWindow: starting request");
var self = this;
var prom = this._client.paginateEventTimeline(tl.timeline, {
backwards: direction == EventTimeline.BACKWARDS,
limit: size
}).finally(function() {
tl.pendingPaginate = null;
}).then(function(r) {
debuglog("TimelineWindow: request completed with result " + r);
if (!r) {
// end of timeline
return false;
}
// recurse to advance the index into the results.
//
// If we don't get any new events, we want to make sure we keep asking
// the server for events for as long as we have a valid pagination
// token. In particular, we want to know if we've actually hit the
// start of the timeline, or if we just happened to know about all of
// the events thanks to https://matrix.org/jira/browse/SYN-645.
//
// On the other hand, we necessarily want to wait forever for the
// server to make its mind up about whether there are other events,
// because it gives a bad user experience
// (https://github.com/vector-im/vector-web/issues/1204).
return self.paginate(direction, size, true, requestLimit - 1);
});
tl.pendingPaginate = prom;
return prom;
};
/**
* Remove `delta` events from the start or end of the timeline.
*
* @param {number} delta number of events to remove from the timeline
* @param {boolean} startOfTimeline if events should be removed from the start
* of the timeline.
*/
TimelineWindow.prototype.unpaginate = function(delta, startOfTimeline) {
var tl = startOfTimeline ? this._start : this._end;
// sanity-check the delta
if (delta > this._eventCount || delta < 0) {
throw new Error("Attemting to unpaginate " + delta + " events, but " +
"only have " + this._eventCount + " in the timeline");
}
while (delta > 0) {
var count = startOfTimeline ? tl.advance(delta) : tl.retreat(delta);
if (count <= 0) {
// sadness. This shouldn't be possible.
throw new Error(
"Unable to unpaginate any further, but still have " +
this._eventCount + " events");
}
delta -= count;
this._eventCount -= count;
debuglog("TimelineWindow.unpaginate: dropped " + count +
" (now " + this._eventCount + ")");
}
};
/**
* Get a list of the events currently in the window
*
* @return {MatrixEvent[]} the events in the window
*/
TimelineWindow.prototype.getEvents = function() {
if (!this._start) {
// not yet loaded
return [];
}
var result = [];
// iterate through each timeline between this._start and this._end
// (inclusive).
var timeline = this._start.timeline;
while (true) {
var events = timeline.getEvents();
// For the first timeline in the chain, we want to start at
// this._start.index. For the last timeline in the chain, we want to
// stop before this._end.index. Otherwise, we want to copy all of the
// events in the timeline.
//
// (Note that both this._start.index and this._end.index are relative
// to their respective timelines' BaseIndex).
//
var startIndex = 0, endIndex = events.length;
if (timeline === this._start.timeline) {
startIndex = this._start.index + timeline.getBaseIndex();
}
if (timeline === this._end.timeline) {
endIndex = this._end.index + timeline.getBaseIndex();
}
for (var i = startIndex; i < endIndex; i++) {
result.push(events[i]);
}
// if we're not done, iterate to the next timeline.
if (timeline === this._end.timeline) {
break;
} else {
timeline = timeline.getNeighbouringTimeline(EventTimeline.FORWARDS);
}
}
return result;
};
/**
* a thing which contains a timeline reference, and an index into it.
*
* @constructor
* @param {EventTimeline} timeline
* @param {number} index
* @private
*/
function TimelineIndex(timeline, index) {
this.timeline = timeline;
// the indexes are relative to BaseIndex, so could well be negative.
this.index = index;
}
/**
* @return {number} the minimum possible value for the index in the current
* timeline
*/
TimelineIndex.prototype.minIndex = function() {
return this.timeline.getBaseIndex() * -1;
};
/**
* @return {number} the maximum possible value for the index in the current
* timeline (exclusive - ie, it actually returns one more than the index
* of the last element).
*/
TimelineIndex.prototype.maxIndex = function() {
return this.timeline.getEvents().length - this.timeline.getBaseIndex();
};
/**
* Try move the index forward, or into the neighbouring timeline
*
* @param {number} delta number of events to advance by
* @return {number} number of events successfully advanced by
*/
TimelineIndex.prototype.advance = function(delta) {
if (!delta) {
return 0;
}
// first try moving the index in the current timeline. See if there is room
// to do so.
var cappedDelta;
if (delta < 0) {
// we want to wind the index backwards.
//
// (this.minIndex() - this.index) is a negative number whose magnitude
// is the amount of room we have to wind back the index in the current
// timeline. We cap delta to this quantity.
cappedDelta = Math.max(delta, this.minIndex() - this.index);
if (cappedDelta < 0) {
this.index += cappedDelta;
return cappedDelta;
}
} else {
// we want to wind the index forwards.
//
// (this.maxIndex() - this.index) is a (positive) number whose magnitude
// is the amount of room we have to wind forward the index in the current
// timeline. We cap delta to this quantity.
cappedDelta = Math.min(delta, this.maxIndex() - this.index);
if (cappedDelta > 0) {
this.index += cappedDelta;
return cappedDelta;
}
}
// the index is already at the start/end of the current timeline.
//
// next see if there is a neighbouring timeline to switch to.
var neighbour = this.timeline.getNeighbouringTimeline(
delta < 0 ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS);
if (neighbour) {
this.timeline = neighbour;
if (delta < 0) {
this.index = this.maxIndex();
} else {
this.index = this.minIndex();
}
debuglog("paginate: switched to new neighbour");
// recurse, using the next timeline
return this.advance(delta);
}
return 0;
};
/**
* Try move the index backwards, or into the neighbouring timeline
*
* @param {number} delta number of events to retreat by
* @return {number} number of events successfully retreated by
*/
TimelineIndex.prototype.retreat = function(delta) {
return this.advance(delta * -1) * -1;
};
/**
* The TimelineWindow class.
*/
module.exports.TimelineWindow = TimelineWindow;
/**
* The TimelineIndex class. exported here for unit testing.
*/
module.exports.TimelineIndex = TimelineIndex;
+341 -3
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module.
@@ -152,19 +167,22 @@ module.exports.findElement = function(array, fn, reverse) {
*/
module.exports.removeElement = function(array, fn, reverse) {
var i;
var removed;
if (reverse) {
for (i = array.length - 1; i >= 0; i--) {
if (fn(array[i], i, array)) {
removed = array[i];
array.splice(i, 1);
return true;
return removed;
}
}
}
else {
for (i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
removed = array[i];
array.splice(i, 1);
return true;
return removed;
}
}
}
@@ -186,7 +204,8 @@ module.exports.isFunction = function(value) {
* @return {boolean} True if it is an array.
*/
module.exports.isArray = function(value) {
return Boolean(value && value.constructor === Array);
return Array.isArray ? Array.isArray(value) :
Boolean(value && value.constructor === Array);
};
/**
@@ -228,6 +247,325 @@ module.exports.deepCopy = function(obj) {
return JSON.parse(JSON.stringify(obj));
};
/**
* Compare two objects for equality. The objects MUST NOT have circular references.
*
* @param {Object} x The first object to compare.
* @param {Object} y The second object to compare.
*
* @return {boolean} true if the two objects are equal
*/
var deepCompare = module.exports.deepCompare = function(x, y) {
// Inspired by
// http://stackoverflow.com/questions/1068834/object-comparison-in-javascript#1144249
// Compare primitives and functions.
// Also check if both arguments link to the same object.
if (x === y) {
return true;
}
if (typeof x !== typeof y) {
return false;
}
// special-case NaN (since NaN !== NaN)
if (typeof x === 'number' && isNaN(x) && isNaN(y)) {
return true;
}
// special-case null (since typeof null == 'object', but null.constructor
// throws)
if (x === null || y === null) {
return x === y;
}
// everything else is either an unequal primitive, or an object
if (!(x instanceof Object)) {
return false;
}
// check they are the same type of object
if (x.constructor !== y.constructor || x.prototype !== y.prototype) {
return false;
}
// special-casing for some special types of object
if (x instanceof RegExp || x instanceof Date) {
return x.toString() === y.toString();
}
// the object algorithm works for Array, but it's sub-optimal.
if (x instanceof Array) {
if (x.length !== y.length) {
return false;
}
for (var i = 0; i < x.length; i++) {
if (!deepCompare(x[i], y[i])) {
return false;
}
}
} else {
// disable jshint "The body of a for in should be wrapped in an if
// statement"
/* jshint -W089 */
// check that all of y's direct keys are in x
var p;
for (p in y) {
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
}
// finally, compare each of x's keys with y
for (p in y) {
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
if (!deepCompare(x[p], y[p])) {
return false;
}
}
}
/* jshint +W089 */
return true;
};
/**
* Copy properties from one object to another.
*
* All enumerable properties, included inherited ones, are copied.
*
* This is approximately equivalent to ES6's Object.assign, except
* that the latter doesn't copy inherited properties.
*
* @param {Object} target The object that will receive new properties
* @param {...Object} source Objects from which to copy properties
*
* @return {Object} target
*/
module.exports.extend = function() {
var target = arguments[0] || {};
// disable jshint "The body of a for in should be wrapped in an if
// statement"
/* jshint -W089 */
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var propName in source) {
target[propName] = source[propName];
}
}
/* jshint +W089 */
return target;
};
/**
* Run polyfills to add Array.map and Array.filter if they are missing.
*/
module.exports.runPolyfills = function() {
// Array.prototype.filter
// ========================================================
// SOURCE:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
if (!Array.prototype.filter) {
Array.prototype.filter = function(fun/*, thisArg*/) {
if (this === void 0 || this === null) {
throw new TypeError();
}
var t = Object(this);
var len = t.length >>> 0;
if (typeof fun !== 'function') {
throw new TypeError();
}
var res = [];
var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
for (var i = 0; i < len; i++) {
if (i in t) {
var val = t[i];
// NOTE: Technically this should Object.defineProperty at
// the next index, as push can be affected by
// properties on Object.prototype and Array.prototype.
// But that method's new, and collisions should be
// rare, so use the more-compatible alternative.
if (fun.call(thisArg, val, i, t)) {
res.push(val);
}
}
}
return res;
};
}
// Array.prototype.map
// ========================================================
// SOURCE:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
// Production steps of ECMA-262, Edition 5, 15.4.4.19
// Reference: http://es5.github.io/#x15.4.4.19
if (!Array.prototype.map) {
Array.prototype.map = function(callback, thisArg) {
var T, A, k;
if (this === null || this === undefined) {
throw new TypeError(' this is null or not defined');
}
// 1. Let O be the result of calling ToObject passing the |this|
// value as the argument.
var O = Object(this);
// 2. Let lenValue be the result of calling the Get internal
// method of O with the argument "length".
// 3. Let len be ToUint32(lenValue).
var len = O.length >>> 0;
// 4. If IsCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (arguments.length > 1) {
T = thisArg;
}
// 6. Let A be a new array created as if by the expression new Array(len)
// where Array is the standard built-in constructor with that name and
// len is the value of len.
A = new Array(len);
// 7. Let k be 0
k = 0;
// 8. Repeat, while k < len
while (k < len) {
var kValue, mappedValue;
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
// b. Let kPresent be the result of calling the HasProperty internal
// method of O with argument Pk.
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {
// i. Let kValue be the result of calling the Get internal
// method of O with argument Pk.
kValue = O[k];
// ii. Let mappedValue be the result of calling the Call internal
// method of callback with T as the this value and argument
// list containing kValue, k, and O.
mappedValue = callback.call(T, kValue, k, O);
// iii. Call the DefineOwnProperty internal method of A with arguments
// Pk, Property Descriptor
// { Value: mappedValue,
// Writable: true,
// Enumerable: true,
// Configurable: true },
// and false.
// In browsers that support Object.defineProperty, use the following:
// Object.defineProperty(A, k, {
// value: mappedValue,
// writable: true,
// enumerable: true,
// configurable: true
// });
// For best browser support, use the following:
A[k] = mappedValue;
}
// d. Increase k by 1.
k++;
}
// 9. return A
return A;
};
}
// Array.prototype.forEach
// ========================================================
// SOURCE:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
// Production steps of ECMA-262, Edition 5, 15.4.4.18
// Reference: http://es5.github.io/#x15.4.4.18
if (!Array.prototype.forEach) {
Array.prototype.forEach = function(callback, thisArg) {
var T, k;
if (this === null || this === undefined) {
throw new TypeError(' this is null or not defined');
}
// 1. Let O be the result of calling ToObject passing the |this| value as the
// argument.
var O = Object(this);
// 2. Let lenValue be the result of calling the Get internal method of O with the
// argument "length".
// 3. Let len be ToUint32(lenValue).
var len = O.length >>> 0;
// 4. If IsCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
if (typeof callback !== "function") {
throw new TypeError(callback + ' is not a function');
}
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
if (arguments.length > 1) {
T = thisArg;
}
// 6. Let k be 0
k = 0;
// 7. Repeat, while k < len
while (k < len) {
var kValue;
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
// b. Let kPresent be the result of calling the HasProperty internal
// method of O with
// argument Pk.
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {
// i. Let kValue be the result of calling the Get internal method of O with
// argument Pk
kValue = O[k];
// ii. Call the Call internal method of callback with T as the this value and
// argument list containing kValue, k, and O.
callback.call(T, kValue, k, O);
}
// d. Increase k by 1.
k++;
}
// 8. return undefined
};
}
};
/**
* Inherit the prototype methods from one constructor into another. This is a
* port of the Node.js implementation with an Object.create polyfill.
+380 -34
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module. See {@link createNewMatrixCall} for the public API.
@@ -44,6 +59,14 @@ function MatrixCall(opts) {
// possible
this.candidateSendQueue = [];
this.candidateSendTries = 0;
// Lookup from opaque queue ID to a promise for media element operations that
// need to be serialised into a given queue. Store this per-MatrixCall on the
// assumption that multiple matrix calls will never compete for control of the
// same DOM elements.
this.mediaPromises = Object.create(null);
this.screenSharingStream = null;
}
/** The length of time a call can be ringing for. */
MatrixCall.CALL_TIMEOUT_MS = 60000;
@@ -64,6 +87,7 @@ utils.inherits(MatrixCall, EventEmitter);
* @throws If you have not specified a listener for 'error' events.
*/
MatrixCall.prototype.placeVoiceCall = function() {
debuglog("placeVoiceCall");
checkForErrorListener(this);
_placeCallWithConstraints(this, _getUserMediaVideoContraints('voice'));
this.type = 'voice';
@@ -78,6 +102,7 @@ MatrixCall.prototype.placeVoiceCall = function() {
* @throws If you have not specified a listener for 'error' events.
*/
MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoElement) {
debuglog("placeVideoCall");
checkForErrorListener(this);
this.localVideoElement = localVideoElement;
this.remoteVideoElement = remoteVideoElement;
@@ -86,6 +111,125 @@ MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoEle
_tryPlayRemoteStream(this);
};
/**
* Place a screen-sharing call to this room. This includes audio.
* <b>This method is EXPERIMENTAL and subject to change without warning. It
* only works in Google Chrome.</b>
* @param {Element} remoteVideoElement a <code>&lt;video&gt;</code> DOM element
* to render video to.
* @param {Element} localVideoElement a <code>&lt;video&gt;</code> DOM element
* to render the local camera preview.
* @throws If you have not specified a listener for 'error' events.
*/
MatrixCall.prototype.placeScreenSharingCall =
function(remoteVideoElement, localVideoElement)
{
debuglog("placeScreenSharingCall");
checkForErrorListener(this);
var screenConstraints = _getChromeScreenSharingConstraints(this);
if (!screenConstraints) {
return;
}
this.localVideoElement = localVideoElement;
this.remoteVideoElement = remoteVideoElement;
var self = this;
this.webRtc.getUserMedia(screenConstraints, function(stream) {
self.screenSharingStream = stream;
debuglog("Got screen stream, requesting audio stream...");
var audioConstraints = _getUserMediaVideoContraints('voice');
_placeCallWithConstraints(self, audioConstraints);
}, function(err) {
self.emit("error",
callError(
MatrixCall.ERR_NO_USER_MEDIA,
"Failed to get screen-sharing stream: " + err
)
);
});
this.type = 'video';
_tryPlayRemoteStream(this);
};
/**
* Play the given HTMLMediaElement, serialising the operation into a chain
* of promises to avoid racing access to the element
* @param {Element} HTMLMediaElement element to play
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
*/
MatrixCall.prototype.playElement = function(element, queueId) {
console.log("queuing play on " + queueId + " and element " + element);
// XXX: FIXME: Does this leak elements, given the old promises
// may hang around and retain a reference to them?
if (this.mediaPromises[queueId]) {
// XXX: these promises can fail (e.g. by <video/> being unmounted whilst
// pending receiving media to play - e.g. whilst switching between
// rooms before answering an inbound call), and throw unhandled exceptions.
// However, we should soldier on as best we can even if they fail, given
// these failures may be non-fatal (as in the case of unmounts)
this.mediaPromises[queueId] =
this.mediaPromises[queueId].then(function() {
console.log("previous promise completed for " + queueId);
return element.play();
}, function() {
console.log("previous promise failed for " + queueId);
return element.play();
});
}
else {
this.mediaPromises[queueId] = element.play();
}
};
/**
* Pause the given HTMLMediaElement, serialising the operation into a chain
* of promises to avoid racing access to the element
* @param {Element} HTMLMediaElement element to pause
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
*/
MatrixCall.prototype.pauseElement = function(element, queueId) {
console.log("queuing pause on " + queueId + " and element " + element);
if (this.mediaPromises[queueId]) {
this.mediaPromises[queueId] =
this.mediaPromises[queueId].then(function() {
console.log("previous promise completed for " + queueId);
return element.pause();
}, function() {
console.log("previous promise failed for " + queueId);
return element.pause();
});
}
else {
// pause doesn't actually return a promise, but do this for symmetry
// and just in case it does in future.
this.mediaPromises[queueId] = element.pause();
}
};
/**
* Assign the given HTMLMediaElement by setting the .src attribute on it,
* serialising the operation into a chain of promises to avoid racing access
* to the element
* @param {Element} HTMLMediaElement element to pause
* @param {string} src the src attribute value to assign to the element
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
*/
MatrixCall.prototype.assignElement = function(element, src, queueId) {
console.log("queuing assign on " + queueId + " element " + element + " for " + src);
if (this.mediaPromises[queueId]) {
this.mediaPromises[queueId] =
this.mediaPromises[queueId].then(function() {
console.log("previous promise completed for " + queueId);
element.src = src;
}, function() {
console.log("previous promise failed for " + queueId);
element.src = src;
});
}
else {
element.src = src;
}
};
/**
* Retrieve the local <code>&lt;video&gt;</code> DOM element.
* @return {Element} The dom element
@@ -95,13 +239,23 @@ MatrixCall.prototype.getLocalVideoElement = function() {
};
/**
* Retrieve the remote <code>&lt;video&gt;</code> DOM element.
* Retrieve the remote <code>&lt;video&gt;</code> DOM element
* used for playing back video capable streams.
* @return {Element} The dom element
*/
MatrixCall.prototype.getRemoteVideoElement = function() {
return this.remoteVideoElement;
};
/**
* Retrieve the remote <code>&lt;audio&gt;</code> DOM element
* used for playing back audio only streams.
* @return {Element} The dom element
*/
MatrixCall.prototype.getRemoteAudioElement = function() {
return this.remoteAudioElement;
};
/**
* Set the local <code>&lt;video&gt;</code> DOM element. If this call is active,
* video will be rendered to it immediately.
@@ -112,13 +266,15 @@ MatrixCall.prototype.setLocalVideoElement = function(element) {
if (element && this.localAVStream && this.type === 'video') {
element.autoplay = true;
element.src = this.URL.createObjectURL(this.localAVStream);
this.assignElement(element,
this.URL.createObjectURL(this.localAVStream),
"localVideo");
element.muted = true;
var self = this;
setTimeout(function() {
var vel = self.getLocalVideoElement();
if (vel.play) {
vel.play();
self.playElement(vel, "localVideo");
}
}, 0);
}
@@ -126,7 +282,7 @@ MatrixCall.prototype.setLocalVideoElement = function(element) {
/**
* Set the remote <code>&lt;video&gt;</code> DOM element. If this call is active,
* video will be rendered to it immediately.
* the first received video-capable stream will be rendered to it immediately.
* @param {Element} element The <code>&lt;video&gt;</code> DOM element.
*/
MatrixCall.prototype.setRemoteVideoElement = function(element) {
@@ -134,6 +290,18 @@ MatrixCall.prototype.setRemoteVideoElement = function(element) {
_tryPlayRemoteStream(this);
};
/**
* 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);
};
/**
* Configure this call from an invite event. Used by MatrixClient.
* @protected
@@ -170,6 +338,7 @@ MatrixCall.prototype._initWithInvite = function(event) {
if (event.getAge()) {
setTimeout(function() {
if (self.state == 'ringing') {
debuglog("Call invite has expired. Hanging up.");
self.hangupParty = 'remote'; // effectively
setState(self, 'ended');
stopAllMedia(self);
@@ -238,6 +407,7 @@ MatrixCall.prototype._replacedBy = function(newCall) {
}
newCall.localVideoElement = this.localVideoElement;
newCall.remoteVideoElement = this.remoteVideoElement;
newCall.remoteAudioElement = this.remoteAudioElement;
this.successor = newCall;
this.emit("replaced", newCall);
this.hangup(true);
@@ -259,6 +429,60 @@ MatrixCall.prototype.hangup = function(reason, suppressEvent) {
sendEvent(this, 'm.call.hangup', content);
};
/**
* Set whether the local video preview should be muted or not.
* @param {boolean} muted True to mute the local video.
*/
MatrixCall.prototype.setLocalVideoMuted = function(muted) {
if (!this.localAVStream) {
return;
}
setTracksEnabled(this.localAVStream.getVideoTracks(), !muted);
};
/**
* Check if local video is muted.
*
* If there are multiple video tracks, <i>all</i> of the tracks need to be muted
* for this to return true. This means if there are no video tracks, this will
* return true.
* @return {Boolean} True if the local preview video is muted, else false
* (including if the call is not set up yet).
*/
MatrixCall.prototype.isLocalVideoMuted = function() {
if (!this.localAVStream) {
return false;
}
return !isTracksEnabled(this.localAVStream.getVideoTracks());
};
/**
* Set whether the microphone should be muted or not.
* @param {boolean} muted True to mute the mic.
*/
MatrixCall.prototype.setMicrophoneMuted = function(muted) {
if (!this.localAVStream) {
return;
}
setTracksEnabled(this.localAVStream.getAudioTracks(), !muted);
};
/**
* Check if the microphone is muted.
*
* If there are multiple audio tracks, <i>all</i> of the tracks need to be muted
* for this to return true. This means if there are no audio tracks, this will
* return true.
* @return {Boolean} True if the mic is muted, else false (including if the call
* is not set up yet).
*/
MatrixCall.prototype.isMicrophoneMuted = function() {
if (!this.localAVStream) {
return false;
}
return !isTracksEnabled(this.localAVStream.getAudioTracks());
};
/**
* Internal
* @private
@@ -272,28 +496,43 @@ MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
if (this.state == 'ended') {
return;
}
debuglog("_gotUserMediaForInvite -> " + this.type);
var self = this;
var videoEl = this.getLocalVideoElement();
if (videoEl && this.type == 'video') {
videoEl.autoplay = true;
videoEl.src = this.URL.createObjectURL(stream);
if (this.screenSharingStream) {
debuglog("Setting screen sharing stream to the local video element");
this.assignElement(videoEl,
this.URL.createObjectURL(this.screenSharingStream),
"localVideo");
}
else {
this.assignElement(videoEl,
this.URL.createObjectURL(stream),
"localVideo");
}
videoEl.muted = true;
setTimeout(function() {
var vel = self.getLocalVideoElement();
if (vel.play) {
vel.play();
self.playElement(vel, "localVideo");
}
}, 0);
}
this.localAVStream = stream;
var audioTracks = stream.getAudioTracks();
for (var i = 0; i < audioTracks.length; i++) {
audioTracks[i].enabled = true;
}
// why do we enable audio (and only audio) tracks here? -- matthew
setTracksEnabled(stream.getAudioTracks(), true);
this.peerConn = _createPeerConnection(this);
this.peerConn.addStream(stream);
if (this.screenSharingStream) {
console.log("Adding screen-sharing stream to peer connection");
this.peerConn.addStream(this.screenSharingStream);
// let's use this for the local preview...
this.localAVStream = this.screenSharingStream;
}
this.peerConn.createOffer(
hookCallback(self, self._gotLocalOffer),
hookCallback(self, self._getLocalOfferFailed)
@@ -315,21 +554,20 @@ MatrixCall.prototype._gotUserMediaForAnswer = function(stream) {
if (localVidEl && self.type == 'video') {
localVidEl.autoplay = true;
localVidEl.src = self.URL.createObjectURL(stream);
this.assignElement(localVidEl,
this.URL.createObjectURL(stream),
"localVideo");
localVidEl.muted = true;
setTimeout(function() {
var vel = self.getLocalVideoElement();
if (vel.play) {
vel.play();
self.playElement(vel, "localVideo");
}
}, 0);
}
self.localAVStream = stream;
var audioTracks = stream.getAudioTracks();
for (var i = 0; i < audioTracks.length; i++) {
audioTracks[i].enabled = true;
}
setTracksEnabled(stream.getAudioTracks(), true);
self.peerConn.addStream(stream);
var constraints = {
@@ -481,8 +719,9 @@ MatrixCall.prototype._getLocalOfferFailed = function(error) {
/**
* Internal
* @private
* @param {Object} error
*/
MatrixCall.prototype._getUserMediaFailed = function() {
MatrixCall.prototype._getUserMediaFailed = function(error) {
this.emit(
"error",
callError(
@@ -550,31 +789,44 @@ MatrixCall.prototype._onSetRemoteDescriptionError = function(e) {
* @param {Object} event
*/
MatrixCall.prototype._onAddStream = function(event) {
debuglog("Stream added" + event);
debuglog("Stream id " + event.stream.id + " added");
var s = event.stream;
this.remoteAVStream = s;
if (this.direction == 'inbound') {
if (s.getVideoTracks().length > 0) {
this.type = 'video';
} else {
this.type = 'voice';
}
if (s.getVideoTracks().length > 0) {
this.type = 'video';
this.remoteAVStream = s;
this.remoteAStream = s;
} else {
this.type = 'voice';
this.remoteAStream = s;
}
var self = this;
forAllTracksOnStream(s, function(t) {
debuglog("Track id " + t.id + " added");
// not currently implemented in chrome
t.onstarted = hookCallback(self, self._onRemoteStreamTrackStarted);
});
event.stream.onended = hookCallback(self, self._onRemoteStreamEnded);
if (event.stream.oninactive !== undefined) {
event.stream.oninactive = hookCallback(self, self._onRemoteStreamEnded);
}
else {
// onended is deprecated from Chrome 54
event.stream.onended = hookCallback(self, self._onRemoteStreamEnded);
}
// not currently implemented in chrome
event.stream.onstarted = hookCallback(self, self._onRemoteStreamStarted);
_tryPlayRemoteStream(this);
if (this.type === 'video') {
_tryPlayRemoteStream(this);
_tryPlayRemoteAudioStream(this);
}
else {
_tryPlayRemoteAudioStream(this);
}
};
/**
@@ -631,6 +883,21 @@ MatrixCall.prototype._onAnsweredElsewhere = function(msg) {
terminate(this, "remote", "answered_elsewhere", true);
};
var setTracksEnabled = function(tracks, enabled) {
for (var i = 0; i < tracks.length; i++) {
tracks[i].enabled = enabled;
}
};
var isTracksEnabled = function(tracks) {
for (var i = 0; i < tracks.length; i++) {
if (tracks[i].enabled) {
return true; // at least one track is enabled
}
}
return false;
};
var setState = function(self, state) {
var oldState = self.state;
self.state = state;
@@ -662,15 +929,21 @@ var sendCandidate = function(self, content) {
var terminate = function(self, hangupParty, hangupReason, shouldEmit) {
if (self.getRemoteVideoElement()) {
if (self.getRemoteVideoElement().pause) {
self.getRemoteVideoElement().pause();
self.pauseElement(self.getRemoteVideoElement(), "remoteVideo");
}
self.getRemoteVideoElement().src = "";
self.assignElement(self.getRemoteVideoElement(), "", "remoteVideo");
}
if (self.getRemoteAudioElement()) {
if (self.getRemoteAudioElement().pause) {
self.pauseElement(self.getRemoteAudioElement(), "remoteAudio");
}
self.assignElement(self.getRemoteAudioElement(), "", "remoteAudio");
}
if (self.getLocalVideoElement()) {
if (self.getLocalVideoElement().pause) {
self.getLocalVideoElement().pause();
self.pauseElement(self.getLocalVideoElement(), "localVideo");
}
self.getLocalVideoElement().src = "";
self.assignElement(self.getLocalVideoElement(), "", "localVideo");
}
self.hangupParty = hangupParty;
self.hangupReason = hangupReason;
@@ -685,6 +958,7 @@ var terminate = function(self, hangupParty, hangupReason, shouldEmit) {
};
var stopAllMedia = function(self) {
debuglog("stopAllMedia (stream=%s)", self.localAVStream);
if (self.localAVStream) {
forAllTracksOnStream(self.localAVStream, function(t) {
if (t.stop) {
@@ -697,6 +971,16 @@ var stopAllMedia = function(self) {
self.localAVStream.stop();
}
}
if (self.screenSharingStream) {
forAllTracksOnStream(self.screenSharingStream, function(t) {
if (t.stop) {
t.stop();
}
});
if (self.screenSharingStream.stop) {
self.screenSharingStream.stop();
}
}
if (self.remoteAVStream) {
forAllTracksOnStream(self.remoteAVStream, function(t) {
if (t.stop) {
@@ -704,17 +988,46 @@ var stopAllMedia = function(self) {
}
});
}
if (self.remoteAStream) {
forAllTracksOnStream(self.remoteAStream, function(t) {
if (t.stop) {
t.stop();
}
});
}
};
var _tryPlayRemoteStream = function(self) {
if (self.getRemoteVideoElement() && self.remoteAVStream) {
var player = self.getRemoteVideoElement();
player.autoplay = true;
player.src = self.URL.createObjectURL(self.remoteAVStream);
self.assignElement(player,
self.URL.createObjectURL(self.remoteAVStream),
"remoteVideo");
setTimeout(function() {
var vel = self.getRemoteVideoElement();
if (vel.play) {
vel.play();
self.playElement(vel, "remoteVideo");
}
// OpenWebRTC does not support oniceconnectionstatechange yet
if (self.webRtc.isOpenWebRTC()) {
setState(self, 'connected');
}
}, 0);
}
};
var _tryPlayRemoteAudioStream = function(self) {
if (self.getRemoteAudioElement() && self.remoteAStream) {
var player = self.getRemoteAudioElement();
player.autoplay = true;
self.assignElement(player,
self.URL.createObjectURL(self.remoteAStream),
"remoteAudio");
setTimeout(function() {
var ael = self.getRemoteAudioElement();
if (ael.play) {
self.playElement(ael, "remoteAudio");
}
// OpenWebRTC does not support oniceconnectionstatechange yet
if (self.webRtc.isOpenWebRTC()) {
@@ -822,6 +1135,38 @@ var _createPeerConnection = function(self) {
return pc;
};
var _getChromeScreenSharingConstraints = function(call) {
var screen = global.screen;
if (!screen) {
call.emit("error", callError(
MatrixCall.ERR_NO_USER_MEDIA,
"Couldn't determine screen sharing constaints."
));
return;
}
// it won't work at all if you're not on HTTPS so whine whine whine
if (!global.window || global.window.location.protocol !== "https:") {
call.emit("error", callError(
MatrixCall.ERR_NO_USER_MEDIA,
"You need to be using HTTPS to place a screen-sharing call."
));
return;
}
return {
video: {
mandatory: {
chromeMediaSource: "screen",
chromeMediaSourceId: "" + Date.now(),
maxWidth: screen.width,
maxHeight: screen.height,
minFrameRate: 1,
maxFrameRate: 10
}
}
};
};
var _getUserMediaVideoContraints = function(callType) {
switch (callType) {
case 'voice':
@@ -866,6 +1211,7 @@ var forAllTracksOnStream = function(s, f) {
/** The MatrixCall class. */
module.exports.MatrixCall = MatrixCall;
/**
* Create a new Matrix call for the browser.
* @param {MatrixClient} client The client instance to use.
+35 -10
View File
@@ -1,16 +1,17 @@
{
"name": "matrix-js-sdk",
"version": "0.2.2",
"version": "0.7.2",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"test": "istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --forceexit --captureExceptions",
"check": "jasmine-node spec --verbose --junitreport --forceexit --captureExceptions",
"test": "istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --captureExceptions",
"check": "jasmine-node spec --verbose --junitreport --captureExceptions",
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
"build": "jshint -c .jshint lib/ && browserify browser-index.js -o dist/browser-matrix-dev.js --ignore-missing",
"watch": "watchify browser-index.js -o dist/browser-matrix-dev.js -v",
"lint": "jshint -c .jshint lib spec && gjslint --unix_mode --disable 0131,0211,0200,0222 --max_line_length 90 -r spec/ -r lib/",
"release": "npm run build && mkdir dist/$npm_package_version && uglifyjs -c -m -o dist/$npm_package_version/browser-matrix-$npm_package_version.min.js dist/browser-matrix-dev.js && cp dist/browser-matrix-dev.js dist/$npm_package_version/browser-matrix-$npm_package_version.js"
"build": "jshint -c .jshint lib/ && rimraf dist && mkdir dist && browserify --exclude olm browser-index.js -o dist/browser-matrix.js --ignore-missing && uglifyjs -c -m -o dist/browser-matrix.min.js dist/browser-matrix.js",
"dist": "npm run build",
"watch": "watchify --exclude olm browser-index.js -o dist/browser-matrix-dev.js -v",
"lint": "jshint -c .jshint lib spec && gjslint --unix_mode --disable 0131,0211,0200,0222,0212 --max_line_length 90 -r spec/ -r lib/",
"prepublish": "git rev-parse HEAD > git-revision.txt"
},
"repository": {
"url": "https://github.com/matrix-org/matrix-js-sdk"
@@ -20,17 +21,41 @@
],
"browser": "browser-index.js",
"author": "matrix.org",
"license": "Apache 2.0",
"license": "Apache-2.0",
"files": [
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
"README.md",
"RELEASING.md",
"examples",
"git-hooks",
"git-revision.txt",
"index.js",
"browser-index.js",
"jenkins.sh",
"lib",
"package.json",
"release.sh",
"spec"
],
"dependencies": {
"another-json": "^0.2.0",
"browser-request": "^0.3.3",
"browserify": "^10.2.3",
"q": "^1.4.1",
"request": "^2.53.0"
},
"devDependencies": {
"watchify": "^3.2.1",
"istanbul": "^0.3.13",
"jasmine-node": "^1.14.5",
"jshint": "^2.8.0"
"jsdoc": "^3.4.0",
"jshint": "^2.8.0",
"rimraf": "^2.5.4",
"uglifyjs": "^2.4.10",
"watchify": "^3.2.1"
},
"optionalDependencies": {
"olm": "https://matrix.org/packages/npm/olm/olm-2.0.0.tgz"
}
}
Executable
+237
View File
@@ -0,0 +1,237 @@
#!/bin/bash
#
# Script to perform a release of matrix-js-sdk. Performs the steps documented
# in RELEASING.md
#
# Requires:
# github-changelog-generator; to install, do
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
# jq; install from your distibution's package manager (https://stedolan.github.io/jq/)
set -e
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
hub --version > /dev/null || (echo "hub is required: please install it"; kill $$)
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
help() {
cat <<EOF
$USAGE
-c changelog_file: specify name of file containing changelog
-x: skip updating the changelog
-z: skip generating the jsdoc
EOF
}
ret=0
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
if [ "$ret" -eq 0 ]; then
echo "package.json contains develop dependencies. Refusing to release."
exit
fi
skip_changelog=
skip_jsdoc=
changelog_file="CHANGELOG.md"
while getopts hc:xz f; do
case $f in
h)
help
exit 0
;;
c)
changelog_file="$OPTARG"
;;
x)
skip_changelog=1
;;
z)
skip_jsdoc=1
;;
esac
done
shift `expr $OPTIND - 1`
if [ $# -ne 1 ]; then
echo "Usage: $USAGE" >&2
exit 1
fi
if [ -z "$skip_changelog" ]; then
# update_changelog doesn't have a --version flag
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
fi
latest_changes=`mktemp`
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}"
# ignore leading v on release
release="${1#v}"
tag="v${release}"
rel_branch="release-$tag"
prerelease=0
# We check if this build is a prerelease by looking to
# see if the version has a hyphen in it. Crude,
# but semver doesn't support postreleases so anything
# with a hyphen is a prerelease.
echo $release | grep -q '-' && prerelease=1
if [ $prerelease -eq 1 ]; then
echo Making a PRE-RELEASE
fi
if [ -z "$skip_changelog" ]; then
if ! command -v update_changelog >/dev/null 2>&1; then
echo "release.sh requires github-changelog-generator. Try:" >&2
echo " pip install git+https://github.com/matrix-org/github-changelog-generator.git" >&2
exit 1
fi
fi
# we might already be on the release branch, in which case, yay
# If we're on any branch starting with 'release', we don't create
# a separate release branch (this allows us to use the same
# release branch for releases and release candidates).
curbranch=$(git symbolic-ref --short HEAD)
if [[ "$curbranch" != release* ]]; then
echo "Creating release branch"
git checkout -b "$rel_branch"
else
echo "Using current branch ($curbranch) for release"
rel_branch=$curbranch
fi
if [ -z "$skip_changelog" ]; then
echo "Generating changelog"
update_changelog -f "$changelog_file" "$release"
read -p "Edit $changelog_file manually, or press enter to continue " REPLY
if [ -n "$(git ls-files --modified $changelog_file)" ]; then
echo "Committing updated changelog"
git commit "$changelog_file" -m "Prepare changelog for $tag"
fi
fi
set -x
# Bump package.json and build the dist
echo "npm version"
# npm version will automatically commit its modification
# and make a release tag. We don't want it to create the tag
# because it can only sign with the default key, but we can
# only turn off both of these behaviours, so we have to
# manually commit the result.
npm version --no-git-tag-version "$release"
git commit package.json -m "$tag"
# figure out if we should be signing this release
signing_id=
if [ -f release_config.yaml ]; then
signing_id=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']"`
fi
# If there is a 'dist' script in the package.json,
# run it in a separate checkout of the project, then
# upload any files in the 'dist' directory as release
# assets.
# We make a completely separate checkout to be sure
# we're using released versions of the dependencies
# (rather than whatever we're pulling in from npm link)
assets=''
dodist=0
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
if [ $dodist -eq 0 ]; then
projdir=`pwd`
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
echo "Building distribution copy in $builddir"
pushd "$builddir"
git clone "$projdir" .
git checkout "$rel_branch"
npm install
# We haven't tagged yet, so tell the dist script what version
# it's building
DIST_VERSION="$tag" npm run dist
popd
for i in "$builddir"/dist/*; do
assets="$assets -a $i"
if [ -n "$signing_id" ]
then
gpg -u "$signing_id" --armor --output "$i".asc --detach-sig "$i"
assets="$assets -a $i.asc"
fi
done
fi
# push the release branch (github can't release from
# a branch it doesn't have)
git push origin "$rel_branch"
if [ -n "$signing_id" ]; then
# make a signed tag
# gnupg seems to fail to get the right tty device unless we set it here
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
else
git tag -a -F "${latest_changes}" "$tag"
fi
# push the tag
git push origin "$tag"
hubflags=''
if [ $prerelease -eq 1 ]; then
hubflags='-p'
fi
release_text=`mktemp`
echo "$tag" > "${release_text}"
echo >> "${release_text}"
cat "${latest_changes}" >> "${release_text}"
hub release create $hubflags $assets -f "${release_text}" "$tag"
if [ $dodist -eq 0 ]; then
rm -rf "$builddir"
fi
rm "${release_text}"
rm "${latest_changes}"
if [ -z "$skip_jsdoc" ]; then
echo "generating jsdocs"
npm run gendoc
echo "copying jsdocs to gh-pages branch"
git checkout gh-pages
git pull
cp -a ".jsdoc/matrix-js-sdk/$release" .
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' \
$release index.html
git add "$release"
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
fi
# merge release branch to master
echo "updating master branch"
git checkout master
git pull
git merge --ff-only "$rel_branch"
# push master and docs (if generated) to github
git push origin master
if [ -z "$skip_jsdoc" ]; then
git push origin gh-pages
fi
# publish to npmjs
npm publish
# finally, merge master back onto develop
git checkout develop
git pull
git merge master
git push origin develop
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env python
"""
Outputs the body of the first entry of changelog file on stdin
"""
import re
import sys
found_first_header = False
for line in sys.stdin:
line = line.strip()
if re.match(r"^Changes in \[.*\]", line):
if found_first_header:
break
found_first_header = True
elif not re.match(r"^=+$", line) and len(line) > 0:
print line
+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;
+734 -191
View File
@@ -1,244 +1,787 @@
"use strict";
var sdk = require("../..");
var q = require("q");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
function MockStorageApi() {
this.data = {};
var utils = require("../../lib/utils");
var test_utils = require("../test-utils");
var aliHttpBackend;
var bobHttpBackend;
var aliClient;
var roomId = "!room:localhost";
var aliUserId = "@ali:localhost";
var aliDeviceId = "zxcvb";
var aliAccessToken = "aseukfgwef";
var bobClient;
var bobUserId = "@bob:localhost";
var bobDeviceId = "bvcxz";
var bobAccessToken = "fewgfkuesa";
var bobOneTimeKeys;
var aliDeviceKeys;
var bobDeviceKeys;
var bobDeviceCurve25519Key;
var bobDeviceEd25519Key;
var aliStorage;
var bobStorage;
var aliMessages;
var bobMessages;
/**
* Set an expectation that the client will upload device keys and a number of
* one-time keys; then flush the http requests.
*
* @param {string} deviceId expected device id in upload request
* @param {object} httpBackend
*
* @return {promise} completes once the http requests have completed, returning combined
* {one_time_keys: {}, device_keys: {}}
*/
function expectKeyUpload(deviceId, httpBackend) {
var uploadPath = "/keys/upload/" + deviceId;
var keys = {};
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.one_time_keys).not.toBeDefined();
expect(content.device_keys).toBeDefined();
keys.device_keys = content.device_keys;
return {one_time_key_counts: {signed_curve25519: 0}};
});
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.device_keys).not.toBeDefined();
expect(content.one_time_keys).toBeDefined();
expect(content.one_time_keys).not.toEqual({});
var count = 0;
for (var key in content.one_time_keys) {
if (content.one_time_keys.hasOwnProperty(key)) {
count++;
}
}
expect(count).toEqual(5);
keys.one_time_keys = content.one_time_keys;
return {one_time_key_counts: {signed_curve25519: count}};
});
return httpBackend.flush(uploadPath, 2).then(function() {
return keys;
});
}
MockStorageApi.prototype = {
setItem: function(k, v) {
this.data[k] = v;
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
}
};
/**
* Set an expectation that ali will upload device keys and a number of one-time keys;
* then flush the http requests.
*
* <p>Updates <tt>aliDeviceKeys</tt>
*
* @return {promise} completes once the http requests have completed.
*/
function expectAliKeyUpload() {
return expectKeyUpload(aliDeviceId, aliHttpBackend).then(function(content) {
aliDeviceKeys = content.device_keys;
});
}
/**
* Set an expectation that bob will upload device keys and a number of one-time keys;
* then flush the http requests.
*
* <p>Updates <tt>bobDeviceKeys</tt>, <tt>bobOneTimeKeys</tt>,
* <tt>bobDeviceCurve25519Key</tt>, <tt>bobDeviceEd25519Key</tt>
*
* @return {promise} completes once the http requests have completed.
*/
function expectBobKeyUpload() {
return expectKeyUpload(bobDeviceId, bobHttpBackend).then(function(content) {
bobDeviceKeys = content.device_keys;
bobOneTimeKeys = content.one_time_keys;
expect(bobDeviceKeys).toBeDefined();
expect(bobOneTimeKeys).toBeDefined();
bobDeviceCurve25519Key = bobDeviceKeys.keys["curve25519:bvcxz"];
bobDeviceEd25519Key = bobDeviceKeys.keys["ed25519:bvcxz"];
});
}
function bobUploadsKeys() {
bobClient.uploadKeys(5).catch(test_utils.failTest);
return expectBobKeyUpload();
}
/**
* Set an expectation that ali will query bobs keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
*/
function expectAliQueryKeys() {
// can't query keys before bob has uploaded them
expect(bobDeviceKeys).toBeDefined();
var bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual({});
var result = {};
result[bobUserId] = bobKeys;
return {device_keys: result};
});
return aliHttpBackend.flush("/keys/query", 1);
}
/**
* Set an expectation that bob will query alis keys; then flush the http request.
*
* @return {promise} which resolves once the http request has completed.
*/
function expectBobQueryKeys() {
// can't query keys before ali has uploaded them
expect(aliDeviceKeys).toBeDefined();
var aliKeys = {};
aliKeys[aliDeviceId] = aliDeviceKeys;
bobHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[aliUserId]).toEqual({});
var result = {};
result[aliUserId] = aliKeys;
return {device_keys: result};
});
return bobHttpBackend.flush("/keys/query", 1);
}
/**
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
*/
function expectAliClaimKeys() {
// can't query keys before bob has uploaded them
expect(bobOneTimeKeys).toBeDefined();
aliHttpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
var claimType = content.one_time_keys[bobUserId][bobDeviceId];
expect(claimType).toEqual("signed_curve25519");
for (var keyId in bobOneTimeKeys) {
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
if (keyId.indexOf(claimType + ":") === 0) {
break;
}
}
}
var result = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = bobOneTimeKeys[keyId];
return {one_time_keys: result};
});
return aliHttpBackend.flush("/keys/claim", 1);
}
function aliDownloadsKeys() {
// can't query keys before bob has uploaded them
expect(bobDeviceEd25519Key).toBeDefined();
var p1 = aliClient.downloadKeys([bobUserId]).then(function() {
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([{
id: "bvcxz",
key: bobDeviceEd25519Key,
verified: false,
blocked: false,
display_name: null,
}]);
});
var p2 = expectAliQueryKeys();
// check that the localStorage is updated as we expect (not sure this is
// an integration test, but meh)
return q.all([p1, p2]).then(function() {
var devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
expect(devices[bobDeviceId].keys).toEqual(bobDeviceKeys.keys);
expect(devices[bobDeviceId].verified).
toBe(0); // DeviceVerification.UNVERIFIED
});
}
function aliEnablesEncryption() {
return aliClient.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
}).then(function() {
expect(aliClient.isRoomEncrypted(roomId)).toBeTruthy();
});
}
function bobEnablesEncryption() {
return bobClient.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
}).then(function() {
expect(bobClient.isRoomEncrypted(roomId)).toBeTruthy();
});
}
/**
* Ali sends a message, first claiming e2e keys. Set the expectations and
* check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function aliSendsFirstMessage() {
return q.all([
sendMessage(aliClient),
expectAliQueryKeys()
.then(expectAliClaimKeys)
.then(expectAliSendMessageRequest)
]).spread(function(_, ciphertext) {
return ciphertext;
});
}
/**
* Ali sends a message without first claiming e2e keys. Set the expectations
* and check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function aliSendsMessage() {
return q.all([
sendMessage(aliClient),
expectAliSendMessageRequest()
]).spread(function(_, ciphertext) {
return ciphertext;
});
}
/**
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
* expectations and check the results.
*
* @return {promise} which resolves to the ciphertext for Ali's device.
*/
function bobSendsReplyMessage() {
return q.all([
sendMessage(bobClient),
expectBobQueryKeys()
.then(expectBobSendMessageRequest)
]).spread(function(_, ciphertext) {
return ciphertext;
});
}
/**
* Set an expectation that Ali will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function expectAliSendMessageRequest() {
return expectSendMessageRequest(aliHttpBackend).then(function(content) {
aliMessages.push(content);
expect(utils.keys(content.ciphertext)).toEqual([bobDeviceCurve25519Key]);
var ciphertext = content.ciphertext[bobDeviceCurve25519Key];
expect(ciphertext).toBeDefined();
return ciphertext;
});
}
/**
* Set an expectation that Bob will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function expectBobSendMessageRequest() {
return expectSendMessageRequest(bobHttpBackend).then(function(content) {
bobMessages.push(content);
var aliKeyId = "curve25519:" + aliDeviceId;
var aliDeviceCurve25519Key = aliDeviceKeys.keys[aliKeyId];
expect(utils.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
var ciphertext = content.ciphertext[aliDeviceCurve25519Key];
expect(ciphertext).toBeDefined();
return ciphertext;
});
}
function sendMessage(client) {
return client.sendMessage(
roomId, {msgtype: "m.text", body: "Hello, World"}
);
}
function expectSendMessageRequest(httpBackend) {
var path = "/send/m.room.encrypted/";
var sent;
httpBackend.when("PUT", path).respond(200, function(path, content) {
sent = content;
return {
event_id: "asdfgh",
};
});
return httpBackend.flush(path, 1).then(function() {
return sent;
});
}
function aliRecvMessage() {
var message = bobMessages.shift();
return recvMessage(aliHttpBackend, aliClient, bobUserId, message);
}
function bobRecvMessage() {
var message = aliMessages.shift();
return recvMessage(bobHttpBackend, bobClient, aliUserId, message);
}
function recvMessage(httpBackend, client, sender, message) {
var syncData = {
next_batch: "x",
rooms: {
join: {
}
}
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
test_utils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: sender,
})
]
}
};
httpBackend.when("GET", "/sync").respond(200, syncData);
var deferred = q.defer();
var onEvent = function(event) {
console.log(client.credentials.userId + " received event",
event);
// ignore the m.room.member events
if (event.getType() == "m.room.member") {
return;
}
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toEqual({
msgtype: "m.text",
body: "Hello, World"
});
expect(event.isEncrypted()).toBeTruthy();
client.removeListener("event", onEvent);
deferred.resolve();
};
client.on("event", onEvent);
httpBackend.flush();
return deferred.promise;
}
function aliStartClient() {
expectAliKeyUpload().catch(test_utils.failTest);
// ali will try to query her own keys on start
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[aliUserId]).toEqual({});
var result = {};
result[aliUserId] = {};
return {device_keys: result};
});
startClient(aliHttpBackend, aliClient);
return aliHttpBackend.flush().then(function() {
console.log("Ali client started");
});
}
function bobStartClient() {
expectBobKeyUpload().catch(test_utils.failTest);
// bob will try to query his own keys on start
bobHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual({});
var result = {};
result[bobUserId] = {};
return {device_keys: result};
});
startClient(bobHttpBackend, bobClient);
return bobHttpBackend.flush().then(function() {
console.log("Bob client started");
});
}
/**
* Set http responses for the requests which are made when a client starts, and
* start the client.
*
* @param {object} httpBackend
* @param {MatrixClient} client
*/
function startClient(httpBackend, client) {
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
// send a sync response including our test room.
var syncData = {
next_batch: "x",
rooms: {
join: { }
}
};
syncData.rooms.join[roomId] = {
state: {
events: [
test_utils.mkMembership({
mship: "join",
user: aliUserId,
}),
test_utils.mkMembership({
mship: "join",
user: bobUserId,
}),
]
},
timeline: {
events: []
}
};
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
}
describe("MatrixClient crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
var baseUrl = "http://localhost.or.something";
var httpBackend;
var aliClient;
var roomId = "!room:localhost";
var aliUserId = "@ali:localhost";
var aliDeviceId = "zxcvb";
var aliAccessToken = "aseukfgwef";
var bobClient;
var bobUserId = "@bob:localhost";
var bobDeviceId = "bvcxz";
var bobAccessToken = "fewgfkuesa";
var bobOneTimeKeys;
var bobDeviceKeys;
var bobDeviceCurve25519Key;
var bobDeviceEd25519Key;
var aliLocalStore;
var aliStorage;
var bobStorage;
var aliMessage;
beforeEach(function() {
aliLocalStore = new MockStorageApi();
aliStorage = new sdk.WebStorageSessionStore(aliLocalStore);
bobStorage = new sdk.WebStorageSessionStore(new MockStorageApi());
utils.beforeEach(this);
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
test_utils.beforeEach(this);
aliStorage = new sdk.WebStorageSessionStore(new test_utils.MockStorageApi());
aliHttpBackend = new HttpBackend();
aliClient = sdk.createClient({
baseUrl: baseUrl,
baseUrl: "http://alis.server",
userId: aliUserId,
accessToken: aliAccessToken,
deviceId: aliDeviceId,
sessionStore: aliStorage
sessionStore: aliStorage,
request: aliHttpBackend.requestFn,
});
bobStorage = new sdk.WebStorageSessionStore(new test_utils.MockStorageApi());
bobHttpBackend = new HttpBackend();
bobClient = sdk.createClient({
baseUrl: baseUrl,
baseUrl: "http://bobs.server",
userId: bobUserId,
accessToken: bobAccessToken,
deviceId: bobDeviceId,
sessionStore: bobStorage
sessionStore: bobStorage,
request: bobHttpBackend.requestFn,
});
httpBackend.when("GET", "/pushrules").respond(200, {});
bobOneTimeKeys = undefined;
aliDeviceKeys = undefined;
bobDeviceKeys = undefined;
bobDeviceCurve25519Key = undefined;
bobDeviceEd25519Key = undefined;
aliMessages = [];
bobMessages = [];
});
describe("Ali account setup", function() {
it("should have device keys", function(done) {
expect(aliClient.deviceKeys).toBeDefined();
expect(aliClient.deviceKeys.user_id).toEqual(aliUserId);
expect(aliClient.deviceKeys.device_id).toEqual(aliDeviceId);
done();
});
it("should have a curve25519 key", function(done) {
expect(aliClient.deviceCurve25519Key).toBeDefined();
done();
});
afterEach(function() {
aliClient.stopClient();
bobClient.stopClient();
});
function bobUploadsKeys(done) {
var uploadPath = "/keys/upload/bvcxz";
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.one_time_keys).toEqual({});
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.one_time_keys).not.toEqual({});
bobDeviceKeys = content.device_keys;
bobOneTimeKeys = content.one_time_keys;
var count = 0;
for (var key in content.one_time_keys) {
if (content.one_time_keys.hasOwnProperty(key)) {
count++;
}
it('Ali knows the difference between a new user and one with no devices',
function(done) {
aliHttpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:id': {},
}
expect(count).toEqual(5);
return {one_time_key_counts: {curve25519: count}};
});
return {one_time_key_counts: {}};
});
bobClient.uploadKeys(5);
httpBackend.flush().done(function() {
expect(bobDeviceKeys).toBeDefined();
expect(bobOneTimeKeys).toBeDefined();
bobDeviceCurve25519Key = bobDeviceKeys.keys["curve25519:bvcxz"];
bobDeviceEd25519Key = bobDeviceKeys.keys["ed25519:bvcxz"];
done();
});
}
it("Bob uploads without one-time keys and with one-time keys", bobUploadsKeys);
var p1 = aliClient.downloadKeys(['@bob:id']);
var p2 = aliHttpBackend.flush('/keys/query', 1);
q.all([p1, p2]).then(function() {
var devices = aliStorage.getEndToEndDevicesForUser('@bob:id');
expect(utils.keys(devices).length).toEqual(0);
// request again: should be no more requests
return aliClient.downloadKeys(['@bob:id']);
}).nodeify(done);
}
);
it("Bob uploads without one-time keys and with one-time keys", function(done) {
q()
.then(bobUploadsKeys)
.catch(test_utils.failTest).done(done);
});
it("Ali downloads Bobs keys", function(done) {
q()
.then(bobUploadsKeys)
.then(aliDownloadsKeys)
.catch(test_utils.failTest).done(done);
});
it("Ali gets keys with an invalid signature", function(done) {
q()
.then(bobUploadsKeys)
.then(function() {
// tamper bob's keys!
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeDefined();
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
return q.all(aliClient.downloadKeys([bobUserId]),
expectAliQueryKeys());
})
.then(function() {
// should get an empty list
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
})
.catch(test_utils.failTest).done(done);
});
it("Ali gets keys with an incorrect userId", function(done) {
var eveUserId = "@eve:localhost";
var bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'bvcxz',
keys: {
'ed25519:bvcxz': 'pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q',
'curve25519:bvcxz': '7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ',
},
user_id: '@eve:localhost',
signatures: {
'@eve:localhost': {
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
},
},
};
function aliDownloadsKeys(done) {
var bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
httpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual({});
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
var result = {};
result[bobUserId] = bobKeys;
return {device_keys: result};
});
aliClient.downloadKeys([bobUserId]).then(function() {
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([{
id: "bvcxz",
key: bobDeviceEd25519Key
}]);
});
httpBackend.flush().done(function() {
var devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
expect(devices).toEqual(bobKeys);
done();
});
}
it("Ali downloads Bobs keys", function(done) {
bobUploadsKeys(function() {aliDownloadsKeys(done);});
q.all(
aliClient.downloadKeys([bobUserId, eveUserId]),
aliHttpBackend.flush("/keys/query", 1)
).then(function() {
// should get an empty list
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
expect(aliClient.listDeviceKeys(eveUserId)).toEqual([]);
}).catch(test_utils.failTest).done(done);
});
function aliEnablesEncryption(done) {
httpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
expect(content.one_time_keys[bobUserId][bobDeviceId]).toEqual("curve25519");
for (var keyId in bobOneTimeKeys) {
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
if (keyId.indexOf("curve25519:") === 0) {
break;
}
}
}
it("Ali gets keys with an incorrect deviceId", function(done) {
var bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'bad_device',
keys: {
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0',
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc',
},
user_id: '@bob:localhost',
signatures: {
'@bob:localhost': {
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
},
},
};
var bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
var result = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = bobOneTimeKeys[keyId];
return {one_time_keys: result};
result[bobUserId] = bobKeys;
return {device_keys: result};
});
aliClient.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
members: [aliUserId, bobUserId]
}).then(function(res) {
expect(res.missingUsers).toEqual([]);
expect(res.missingDevices).toEqual({});
expect(aliClient.isRoomEncrypted(roomId)).toBeTruthy();
done();
});
httpBackend.flush();
}
q.all(
aliClient.downloadKeys([bobUserId]),
aliHttpBackend.flush("/keys/query", 1)
).then(function() {
// should get an empty list
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
}).catch(test_utils.failTest).done(done);
});
it("Ali enables encryption", function(done) {
bobUploadsKeys(function() {
aliDownloadsKeys(function() {
aliEnablesEncryption(done);
});
});
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.catch(test_utils.failTest).done(done);
});
function aliSendsMessage(done) {
var txnId = "a.transaction.id";
var path = "/send/m.room.encrypted/" + txnId;
httpBackend.when("PUT", path).respond(200, function(path, content) {
aliMessage = content;
expect(aliMessage.ciphertext[bobDeviceCurve25519Key]).toBeDefined();
return {};
});
aliClient.sendMessage(
roomId, {msgtype: "m.text", body: "Hello, World"}, txnId
);
httpBackend.flush().done(function() {done();});
}
it("Ali sends a message", function(done) {
bobUploadsKeys(function() {
aliDownloadsKeys(function() {
aliEnablesEncryption(function() {
aliSendsMessage(done);
});
});
});
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.catch(test_utils.failTest).nodeify(done);
});
function bobRecvMessage(done) {
var initialSync = {
end: "alpha",
presence: [],
rooms: []
};
var events = {
start: "alpha",
end: "beta",
chunk: [utils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: aliMessage
})]
};
httpBackend.when("GET", "initialSync").respond(200, initialSync);
httpBackend.when("GET", "events").respond(200, events);
bobClient.on("event", function(event) {
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toEqual({
msgtype: "m.text",
body: "Hello, World"
});
expect(event.isEncrypted()).toBeTruthy();
done();
});
bobClient.startClient();
httpBackend.flush();
}
it("Bob receives a message", function(done) {
bobUploadsKeys(function() {
aliDownloadsKeys(function() {
aliEnablesEncryption(function() {
aliSendsMessage(function() {
bobRecvMessage(done);
});
});
});
});
}, 30000); //timeout after 30s
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobStartClient)
.then(bobRecvMessage)
.catch(test_utils.failTest).done(done);
});
it("Bob receives a message with a bogus sender", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobStartClient)
.then(function() {
var message = aliMessages.shift();
var syncData = {
next_batch: "x",
rooms: {
join: {
}
}
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
test_utils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: "@bogus:sender",
})
]
}
};
bobHttpBackend.when("GET", "/sync").respond(200, syncData);
var deferred = q.defer();
var onEvent = function(event) {
console.log(bobClient.credentials.userId + " received event",
event);
// ignore the m.room.member events
if (event.getType() == "m.room.member") {
return;
}
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
expect(event.isEncrypted()).toBeTruthy();
bobClient.removeListener("event", onEvent);
deferred.resolve();
};
bobClient.on("event", onEvent);
bobHttpBackend.flush();
return deferred.promise;
})
.catch(test_utils.failTest).done(done);
});
it("Ali blocks Bob's device", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliDownloadsKeys)
.then(function() {
aliClient.setDeviceBlocked(bobUserId, bobDeviceId, true);
var p1 = sendMessage(aliClient);
var p2 = expectAliQueryKeys()
.then(expectAliClaimKeys)
.then(function() {
return expectSendMessageRequest(aliHttpBackend);
}).then(function(sentContent) {
// no unblocked devices, so the ciphertext should be empty
expect(sentContent.ciphertext).toEqual({});
});
return q.all([p1, p2]);
}).catch(test_utils.failTest).nodeify(done);
});
it("Bob receives two pre-key messages", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobStartClient)
.then(bobRecvMessage)
.then(aliSendsMessage)
.then(bobRecvMessage)
.catch(test_utils.failTest).done(done);
});
it("Bob replies to the message", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobStartClient)
.then(bobRecvMessage)
.then(bobEnablesEncryption)
.then(bobSendsReplyMessage).then(function(ciphertext) {
expect(ciphertext.type).toEqual(1);
}).then(aliRecvMessage)
.catch(test_utils.failTest).done(done);
});
it("Ali does a key query when she gets a new_device event", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(function() {
var syncData = {
next_batch: '2',
to_device: {
events: [
test_utils.mkEvent({
content: {
device_id: 'TEST_DEVICE',
rooms: [],
},
sender: bobUserId,
type: 'm.new_device',
}),
],
},
};
aliHttpBackend.when('GET', '/sync').respond(200, syncData);
return aliHttpBackend.flush('/sync', 1);
}).then(expectAliQueryKeys)
.nodeify(done);
});
});
+115 -89
View File
@@ -19,115 +19,123 @@ describe("MatrixClient events", function() {
accessToken: selfAccessToken
});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("emissions", function() {
var initialSync = {
end: "s_5_3",
presence: [{
event_id: "$wefiuewh:bar",
type: "m.presence",
content: {
user_id: "@foo:bar",
displayname: "Foo Bar",
presence: "online"
}
}],
rooms: [{
room_id: "!erufh:bar",
membership: "join",
messages: {
start: "s",
end: "t",
chunk: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm"
})
]
},
state: [
utils.mkMembership({
room: "!erufh:bar", mship: "join", user: "@foo:bar"
}),
utils.mkEvent({
type: "m.room.create", room: "!erufh:bar", user: "@foo:bar",
content: {
creator: "@foo:bar"
}
var SYNC_DATA = {
next_batch: "s_5_3",
presence: {
events: [
utils.mkPresence({
user: "@foo:bar", name: "Foo Bar", presence: "online"
})
]
}]
};
var eventData = {
start: "s_5_3",
end: "e_6_7",
chunk: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "ello ello"
}),
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: ":D"
}),
utils.mkEvent({
type: "m.typing", room: "!erufh:bar", content: {
user_ids: ["@foo:bar"]
},
rooms: {
join: {
"!erufh:bar": {
timeline: {
events: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm"
})
],
prev_batch: "s"
},
state: {
events: [
utils.mkMembership({
room: "!erufh:bar", mship: "join", user: "@foo:bar"
}),
utils.mkEvent({
type: "m.room.create", room: "!erufh:bar",
user: "@foo:bar",
content: {
creator: "@foo:bar"
}
})
]
}
}
})
]
}
}
};
var NEXT_SYNC_DATA = {
next_batch: "e_6_7",
rooms: {
join: {
"!erufh:bar": {
timeline: {
events: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "ello ello"
}),
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: ":D"
}),
]
},
ephemeral: {
events: [
utils.mkEvent({
type: "m.typing", room: "!erufh:bar", content: {
user_ids: ["@foo:bar"]
}
})
]
}
}
}
}
};
it("should emit events from both /initialSync and /events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
it("should emit events from both the first and subsequent /sync calls",
function(done) {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var expectedEvents = [];
expectedEvents = expectedEvents.concat(
SYNC_DATA.presence.events,
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events
);
// initial sync events are unordered, so make an array of the types
// that should be emitted and we'll just pick them off one by one,
// so long as this is emptied we're good.
var initialSyncEventTypes = [
"m.presence", "m.room.member", "m.room.message", "m.room.create"
];
var chunkIndex = 0;
client.on("event", function(event) {
if (initialSyncEventTypes.length === 0) {
if (chunkIndex + 1 >= eventData.chunk.length) {
return;
var found = false;
for (var i = 0; i < expectedEvents.length; i++) {
if (expectedEvents[i].event_id === event.getId()) {
expectedEvents.splice(i, 1);
found = true;
break;
}
// this should be /events now
expect(eventData.chunk[chunkIndex].event_id).toEqual(
event.getId()
);
chunkIndex++;
return;
}
var index = initialSyncEventTypes.indexOf(event.getType());
expect(index).not.toEqual(
-1, "Unexpected event type: " + event.getType()
expect(found).toBe(
true, "Unexpected 'event' emitted: " + event.getType()
);
if (index >= 0) {
initialSyncEventTypes.splice(index, 1);
}
});
client.startClient();
httpBackend.flush().done(function() {
expect(initialSyncEventTypes.length).toEqual(
0, "Failed to see all events from /initialSync"
);
expect(chunkIndex + 1).toEqual(
eventData.chunk.length, "Failed to see all events from /events"
expect(expectedEvents.length).toEqual(
0, "Failed to see all events from /sync calls"
);
done();
});
});
it("should emit User events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var fired = false;
client.on("User.presence", function(event, user) {
fired = true;
@@ -135,9 +143,9 @@ describe("MatrixClient events", function() {
expect(event).toBeDefined();
if (!user || !event) { return; }
expect(event.event).toEqual(initialSync.presence[0]);
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
expect(user.presence).toEqual(
initialSync.presence[0].content.presence
SYNC_DATA.presence.events[0].content.presence
);
});
client.startClient();
@@ -149,8 +157,8 @@ describe("MatrixClient events", function() {
});
it("should emit Room events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var roomInvokeCount = 0;
var roomNameInvokeCount = 0;
var timelineFireCount = 0;
@@ -183,8 +191,8 @@ describe("MatrixClient events", function() {
});
it("should emit RoomState events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var roomStateEventTypes = [
"m.room.member", "m.room.create"
@@ -232,8 +240,8 @@ describe("MatrixClient events", function() {
});
it("should emit RoomMember events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var typingInvokeCount = 0;
var powerLevelInvokeCount = 0;
@@ -272,6 +280,24 @@ describe("MatrixClient events", function() {
done();
});
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function(done) {
httpBackend.when("GET", "/sync").respond(401, { errcode: 'M_UNKNOWN_TOKEN' });
var sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(event, member) {
sessionLoggedOutCount++;
});
client.startClient();
httpBackend.flush().done(function() {
expect(sessionLoggedOutCount).toEqual(
1, "Session.logged_out fired wrong number of times"
);
done();
});
});
});
});
@@ -0,0 +1,735 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
var EventTimeline = sdk.EventTimeline;
var baseUrl = "http://localhost.or.something";
var userId = "@alice:localhost";
var userName = "Alice";
var accessToken = "aseukfgwef";
var roomId = "!foo:bar";
var otherUserId = "@bob:localhost";
var USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName
});
var ROOM_NAME_EVENT = utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId,
content: {
name: "Old room name"
}
});
var INITIAL_SYNC_DATA = {
next_batch: "s_5_3",
rooms: {
join: {
"!foo:bar": { // roomId
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello"
})
],
prev_batch: "f_1_1"
},
state: {
events: [
ROOM_NAME_EVENT,
utils.mkMembership({
room: roomId, mship: "join",
user: otherUserId, name: "Bob"
}),
USER_MEMBERSHIP_EVENT,
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}
}
}
}
};
var EVENTS = [
utils.mkMessage({
room: roomId, user: userId, msg: "we",
}),
utils.mkMessage({
room: roomId, user: userId, msg: "could",
}),
utils.mkMessage({
room: roomId, user: userId, msg: "be",
}),
utils.mkMessage({
room: roomId, user: userId, msg: "heroes",
}),
];
// start the client, and wait for it to initialise
function startClient(httpBackend, client) {
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
client.startClient();
// set up a promise which will resolve once the client is initialised
var deferred = q.defer();
client.on("sync", function(state) {
console.log("sync", state);
if (state != "SYNCING") {
return;
}
deferred.resolve();
});
httpBackend.flush();
return deferred.promise;
}
describe("getEventTimeline support", function() {
var httpBackend;
var client;
beforeEach(function() {
utils.beforeEach(this);
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
});
afterEach(function() {
if (client) {
client.stopClient();
}
});
it("timeline support must be enabled to work", function(done) {
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
});
startClient(httpBackend, client
).then(function() {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
expect(function() { client.getEventTimeline(timelineSet, "event"); })
.toThrow();
}).catch(utils.failTest).done(done);
});
it("timeline support works when enabled", function(done) {
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
timelineSupport: true,
});
startClient(httpBackend, client
).then(function() {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
expect(function() { client.getEventTimeline(timelineSet, "event"); })
.not.toThrow();
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
it("scrollback should be able to scroll back to before a gappy /sync",
function(done) {
// need a client with timelineSupport disabled to make this work
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
});
var room;
startClient(httpBackend, client
).then(function() {
room = client.getRoom(roomId);
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
EVENTS[0],
],
prev_batch: "f_1_1",
},
},
},
},
});
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_5",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
EVENTS[1],
],
limited: true,
prev_batch: "f_1_2",
},
},
},
},
});
httpBackend.when("GET", "/messages").respond(200, {
chunk: [EVENTS[0]],
start: "pagin_start",
end: "pagin_end",
});
return httpBackend.flush("/sync", 2);
}).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(EVENTS[1]);
httpBackend.flush("/messages", 1);
return client.scrollback(room);
}).then(function() {
expect(room.timeline.length).toEqual(2);
expect(room.timeline[0].event).toEqual(EVENTS[0]);
expect(room.timeline[1].event).toEqual(EVENTS[1]);
expect(room.oldState.paginationToken).toEqual("pagin_end");
}).catch(utils.failTest).done(done);
});
});
describe("MatrixClient event timelines", function() {
var client, httpBackend;
beforeEach(function(done) {
utils.beforeEach(this);
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
timelineSupport: true,
});
startClient(httpBackend, client)
.catch(utils.failTest).done(done);
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("getEventTimeline", function() {
it("should create a new timeline for new events", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
.respond(200, function() {
return {
start: "start_token",
events_before: [EVENTS[1], EVENTS[0]],
event: EVENTS[2],
events_after: [EVENTS[3]],
state: [
ROOM_NAME_EVENT,
USER_MEMBERSHIP_EVENT,
],
end: "end_token",
};
});
client.getEventTimeline(timelineSet, "event1:bar").then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
for (var i = 0; i < 4; i++) {
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
expect(tl.getEvents()[i].sender.name).toEqual(userName);
}
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
it("should return existing timeline for known events", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
EVENTS[0],
],
prev_batch: "f_1_2",
},
},
},
},
});
httpBackend.flush("/sync").then(function() {
return client.getEventTimeline(timelineSet, EVENTS[0].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(EVENTS[0]);
expect(tl.getEvents()[1].sender.name).toEqual(userName);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("f_1_1");
// expect(tl.getPaginationToken(EventTimeline.FORWARDS)).toEqual("s_5_4");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
it("should update timelines where they overlap a previous /sync", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
EVENTS[3],
],
prev_batch: "f_1_2",
},
},
},
},
});
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[2].event_id))
.respond(200, function() {
return {
start: "start_token",
events_before: [EVENTS[1]],
event: EVENTS[2],
events_after: [EVENTS[3]],
end: "end_token",
state: [],
};
});
client.on("sync", function() {
client.getEventTimeline(timelineSet, EVENTS[2].event_id
).then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
expect(tl.getEvents()[3].event).toEqual(EVENTS[3]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token");
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
// .toEqual("s_5_4");
}).catch(utils.failTest).done(done);
});
httpBackend.flush().catch(utils.failTest);
});
it("should join timelines where they overlap a previous /context",
function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
// with context which joins them all up.
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: EVENTS[0],
events_after: [],
end: "end_token0",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[2].event_id))
.respond(200, function() {
return {
start: "start_token2",
events_before: [],
event: EVENTS[2],
events_after: [],
end: "end_token2",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[3].event_id))
.respond(200, function() {
return {
start: "start_token3",
events_before: [],
event: EVENTS[3],
events_after: [],
end: "end_token3",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[1].event_id))
.respond(200, function() {
return {
start: "start_token4",
events_before: [EVENTS[0]],
event: EVENTS[1],
events_after: [EVENTS[2], EVENTS[3]],
end: "end_token4",
state: [],
};
});
var tl0, tl2, tl3;
client.getEventTimeline(timelineSet, EVENTS[0].event_id
).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl0 = tl;
return client.getEventTimeline(timelineSet, EVENTS[2].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl2 = tl;
return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl3 = tl;
return client.getEventTimeline(timelineSet, EVENTS[1].event_id);
}).then(function(tl) {
// we expect it to get merged in with event 2
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
expect(tl.getNeighbouringTimeline(EventTimeline.BACKWARDS))
.toBe(tl0);
expect(tl.getNeighbouringTimeline(EventTimeline.FORWARDS))
.toBe(tl3);
expect(tl0.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl0.getPaginationToken(EventTimeline.FORWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.BACKWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token3");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
it("should fail gracefully if there is no event field", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
// with context which joins them all up.
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1")
.respond(200, function() {
return {
start: "start_token",
events_before: [],
events_after: [],
end: "end_token",
state: [],
};
});
client.getEventTimeline(timelineSet, "event1"
).then(function(tl) {
// could do with a fail()
expect(true).toBeFalsy();
}).catch(function(e) {
expect(String(e)).toMatch(/'event'/);
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
});
describe("paginateEventTimeline", function() {
it("should allow you to paginate backwards", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: EVENTS[0],
events_after: [],
end: "end_token0",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
.check(function(req) {
var params = req.queryParams;
expect(params.dir).toEqual("b");
expect(params.from).toEqual("start_token0");
expect(params.limit).toEqual(30);
}).respond(200, function() {
return {
chunk: [EVENTS[1], EVENTS[2]],
end: "start_token1",
};
});
var tl;
client.getEventTimeline(timelineSet, EVENTS[0].event_id
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(tl, {backwards: true});
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[2]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[0]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token1");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token0");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
it("should allow you to paginate forwards", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: EVENTS[0],
events_after: [],
end: "end_token0",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
.check(function(req) {
var params = req.queryParams;
expect(params.dir).toEqual("f");
expect(params.from).toEqual("end_token0");
expect(params.limit).toEqual(20);
}).respond(200, function() {
return {
chunk: [EVENTS[1], EVENTS[2]],
end: "end_token1",
};
});
var tl;
client.getEventTimeline(timelineSet, EVENTS[0].event_id
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(
tl, {backwards: false, limit: 20});
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[0]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[2]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token1");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
});
describe("event timeline for sent events", function() {
var TXN_ID = "txn1";
var event = utils.mkMessage({
room: roomId, user: userId, msg: "a body",
});
event.unsigned = {transaction_id: TXN_ID};
beforeEach(function() {
// set up handlers for both the message send, and the
// /sync
httpBackend.when("PUT", "/send/m.room.message/" + TXN_ID)
.respond(200, {
event_id: event.event_id,
});
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
event
],
prev_batch: "f_1_1",
},
},
},
},
});
});
it("should work when /send returns before /sync", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
// 2 because the initial sync contained an event
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
// now let the sync complete, and check it again
return httpBackend.flush("/sync", 1);
}).then(function() {
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
}).catch(utils.failTest).done(done);
httpBackend.flush("/send/m.room.message/" + TXN_ID, 1).catch(utils.failTest);
});
it("should work when /send returns after /sync", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
// initiate the send, and set up checks to be done when it completes
// - but note that it won't complete until after the /sync does, below.
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
console.log("sendTextMessage completed");
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (2)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
}).catch(utils.failTest).done(done);
httpBackend.flush("/sync", 1).then(function() {
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (1)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
// now let the send complete.
return httpBackend.flush("/send/m.room.message/" + TXN_ID, 1);
}).catch(utils.failTest);
});
});
it("should handle gappy syncs after redactions", function(done) {
// https://github.com/vector-im/vector-web/issues/1389
// a state event, followed by a redaction thereof
var event = utils.mkMembership({
room: roomId, mship: "join", user: otherUserId
});
var redaction = utils.mkEvent({
type: "m.room.redaction",
room_id: roomId,
sender: otherUserId,
content: {}
});
redaction.redacts = event.event_id;
var syncData = {
next_batch: "batch1",
rooms: {
join: {},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
event,
redaction,
],
limited: false,
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.flush().then(function() {
var room = client.getRoom(roomId);
var tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[1].isRedacted()).toBe(true);
var sync2 = {
next_batch: "batch2",
rooms: {
join: {},
},
};
sync2.rooms.join[roomId] = {
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "world"
}),
],
limited: true,
prev_batch: "newerTok",
},
};
httpBackend.when("GET", "/sync").respond(200, sync2);
return httpBackend.flush();
}).then(function() {
var room = client.getRoom(roomId);
var tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(1);
}).catch(utils.failTest).done(done);
});
});
+357 -3
View File
@@ -4,11 +4,13 @@ var HttpBackend = require("../mock-request");
var publicGlobals = require("../../lib/matrix");
var Room = publicGlobals.Room;
var MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
var Filter = publicGlobals.Filter;
var utils = require("../test-utils");
var MockStorageApi = require("../MockStorageApi");
describe("MatrixClient", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend, store;
var client, httpBackend, store, sessionStore;
var userId = "@alice:localhost";
var accessToken = "aseukfgwef";
@@ -16,12 +18,18 @@ describe("MatrixClient", function() {
utils.beforeEach(this);
httpBackend = new HttpBackend();
store = new MatrixInMemoryStore();
var mockStorage = new MockStorageApi();
sessionStore = new sdk.WebStorageSessionStore(mockStorage);
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
deviceId: "aliceDevice",
accessToken: accessToken,
store: store
store: store,
sessionStore: sessionStore,
});
});
@@ -29,11 +37,122 @@ describe("MatrixClient", function() {
httpBackend.verifyNoOutstandingExpectation();
});
describe("uploadContent", function() {
var buf = new Buffer('hello world');
it("should upload the file", function(done) {
httpBackend.when(
"POST", "/_matrix/media/v1/upload"
).check(function(req) {
expect(req.data).toEqual(buf);
expect(req.queryParams.filename).toEqual("hi.txt");
expect(req.queryParams.access_token).toEqual(accessToken);
expect(req.headers["Content-Type"]).toEqual("text/plain");
expect(req.opts.json).toBeFalsy();
expect(req.opts.timeout).toBe(undefined);
}).respond(200, "content");
var prom = client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
});
expect(prom).toBeDefined();
var uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(1);
expect(uploads[0].promise).toBe(prom);
expect(uploads[0].loaded).toEqual(0);
prom.then(function(response) {
// for backwards compatibility, we return the raw JSON
expect(response).toEqual("content");
var uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(0);
}).catch(utils.failTest).done(done);
httpBackend.flush();
});
it("should parse the response if rawResponse=false", function(done) {
httpBackend.when(
"POST", "/_matrix/media/v1/upload"
).check(function(req) {
expect(req.opts.json).toBeFalsy();
}).respond(200, JSON.stringify({ "content_uri": "uri" }));
client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
}, {
rawResponse: false,
}).then(function(response) {
expect(response.content_uri).toEqual("uri");
}).catch(utils.failTest).done(done);
httpBackend.flush();
});
it("should parse errors into a MatrixError", function(done) {
// opts.json is false, so request returns unparsed json.
httpBackend.when(
"POST", "/_matrix/media/v1/upload"
).check(function(req) {
expect(req.data).toEqual(buf);
expect(req.opts.json).toBeFalsy();
}).respond(400, JSON.stringify({
"errcode": "M_SNAFU",
"error": "broken",
}));
client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
}).then(function(response) {
throw Error("request not failed");
}, function(error) {
expect(error.httpStatus).toEqual(400);
expect(error.errcode).toEqual("M_SNAFU");
expect(error.message).toEqual("broken");
}).catch(utils.failTest).done(done);
httpBackend.flush();
});
it("should return a promise which can be cancelled", function(done) {
var prom = client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
});
var uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(1);
expect(uploads[0].promise).toBe(prom);
expect(uploads[0].loaded).toEqual(0);
prom.then(function(response) {
throw Error("request not aborted");
}, function(error) {
expect(error).toEqual("aborted");
var uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(0);
}).catch(utils.failTest).done(done);
var r = client.cancelUpload(prom);
expect(r).toBe(true);
});
});
describe("joinRoom", function() {
it("should no-op if you've already joined a room", function() {
var roomId = "!foo:bar";
var room = new Room(roomId);
room.addEvents([
room.addLiveEvents([
utils.mkMembership({
user: userId, room: roomId, mship: "join", event: true
})
@@ -43,4 +162,239 @@ describe("MatrixClient", function() {
httpBackend.verifyNoOutstandingRequests();
});
});
describe("getFilter", function() {
var filterId = "f1lt3r1d";
it("should return a filter from the store if allowCached", function(done) {
var filter = Filter.fromJson(userId, filterId, {
event_format: "client"
});
store.storeFilter(filter);
client.getFilter(userId, filterId, true).done(function(gotFilter) {
expect(gotFilter).toEqual(filter);
done();
});
httpBackend.verifyNoOutstandingRequests();
});
it("should do an HTTP request if !allowCached even if one exists",
function(done) {
var httpFilterDefinition = {
event_format: "federation"
};
httpBackend.when(
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId
).respond(200, httpFilterDefinition);
var storeFilter = Filter.fromJson(userId, filterId, {
event_format: "client"
});
store.storeFilter(storeFilter);
client.getFilter(userId, filterId, false).done(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
done();
});
httpBackend.flush();
});
it("should do an HTTP request if nothing is in the cache and then store it",
function(done) {
var httpFilterDefinition = {
event_format: "federation"
};
expect(store.getFilter(userId, filterId)).toBeNull();
httpBackend.when(
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId
).respond(200, httpFilterDefinition);
client.getFilter(userId, filterId, true).done(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
expect(store.getFilter(userId, filterId)).toBeDefined();
done();
});
httpBackend.flush();
});
});
describe("createFilter", function() {
var filterId = "f1llllllerid";
it("should do an HTTP request and then store the filter", function(done) {
expect(store.getFilter(userId, filterId)).toBeNull();
var filterDefinition = {
event_format: "client"
};
httpBackend.when(
"POST", "/user/" + encodeURIComponent(userId) + "/filter"
).check(function(req) {
expect(req.data).toEqual(filterDefinition);
}).respond(200, {
filter_id: filterId
});
client.createFilter(filterDefinition).done(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(filterDefinition);
expect(store.getFilter(userId, filterId)).toEqual(gotFilter);
done();
});
httpBackend.flush();
});
});
describe("searching", function() {
var response = {
search_categories: {
room_events: {
count: 24,
results: {
"$flibble:localhost": {
rank: 0.1,
result: {
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
body: "a result",
msgtype: "m.text"
}
}
}
}
}
}
};
it("searchMessageText should perform a /search for room_events", function(done) {
client.searchMessageText({
query: "monkeys"
});
httpBackend.when("POST", "/search").check(function(req) {
expect(req.data).toEqual({
search_categories: {
room_events: {
search_term: "monkeys"
}
}
});
}).respond(200, response);
httpBackend.flush().done(function() {
done();
});
});
});
describe("downloadKeys", function() {
it("should do an HTTP request and then store the keys", function(done) {
var ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
// ed25519key = client.getDeviceEd25519Key();
var borisKeys = {
dev1: {
algorithms: ["1"],
device_id: "dev1",
keys: { "ed25519:dev1": ed25519key },
signatures: {
boris: {
"ed25519:dev1":
"RAhmbNDq1efK3hCpBzZDsKoGSsrHUxb25NW5/WbEV9R" +
"JVwLdP032mg5QsKt/pBDUGtggBcnk43n3nBWlA88WAw"
}
},
unsigned: { "abc": "def" },
user_id: "boris",
}
};
var chazKeys = {
dev2: {
algorithms: ["2"],
device_id: "dev2",
keys: { "ed25519:dev2": ed25519key },
signatures: {
chaz: {
"ed25519:dev2":
"FwslH/Q7EYSb7swDJbNB5PSzcbEO1xRRBF1riuijqvL" +
"EkrK9/XVN8jl4h7thGuRITQ01siBQnNmMK9t45QfcCQ"
}
},
unsigned: { "ghi": "def" },
user_id: "chaz",
}
};
/*
function sign(o) {
var anotherjson = require('another-json');
var b = JSON.parse(JSON.stringify(o));
delete(b.signatures);
delete(b.unsigned);
return client._crypto._olmDevice.sign(anotherjson.stringify(b));
};
console.log("Ed25519: " + ed25519key);
console.log("boris:", sign(borisKeys.dev1));
console.log("chaz:", sign(chazKeys.dev2));
*/
httpBackend.when("POST", "/keys/query").check(function(req) {
expect(req.data).toEqual({device_keys: {boris: {}, chaz: {}}});
}).respond(200, {
device_keys: {
boris: borisKeys,
chaz: chazKeys,
},
});
client.downloadKeys(["boris", "chaz"]).then(function(res) {
assertObjectContains(res.boris.dev1, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev1": ed25519key },
algorithms: ["1"],
unsigned: { "abc": "def" },
});
assertObjectContains(res.chaz.dev2, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev2" : ed25519key },
algorithms: ["2"],
unsigned: { "ghi": "def" },
});
}).catch(utils.failTest).done(done);
httpBackend.flush();
});
});
describe("deleteDevice", function() {
var auth = {a: 1};
it("should pass through an auth dict", function(done) {
httpBackend.when(
"DELETE", "/_matrix/client/unstable/devices/my_device"
).check(function(req) {
expect(req.data).toEqual({auth: auth});
}).respond(200);
client.deleteDevice(
"my_device", auth
).catch(utils.failTest).done(done);
httpBackend.flush();
});
});
});
function assertObjectContains(obj, expected) {
for (var k in expected) {
if (expected.hasOwnProperty(k)) {
expect(obj[k]).toEqual(expected[k]);
}
}
}
+46 -44
View File
@@ -11,47 +11,45 @@ describe("MatrixClient opts", function() {
var userB = "@bob:localhost";
var accessToken = "aseukfgwef";
var roomId = "!foo:bar";
var eventData = {
chunk: [],
start: "s",
end: "e"
};
var initialSync = {
end: "s_5_3",
presence: [],
rooms: [{
membership: "join",
room_id: roomId,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage({
room: roomId, user: userB, msg: "hello"
})
]
},
state: [
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB,
content: {
name: "Old room name"
var syncData = {
next_batch: "s_5_3",
presence: {},
rooms: {
join: {
"!foo:bar": { // roomId
timeline: {
events: [
utils.mkMessage({
room: roomId, user: userB, msg: "hello"
})
],
prev_batch: "f_1_1"
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB,
content: {
name: "Old room name"
}
}),
utils.mkMembership({
room: roomId, mship: "join", user: userB, name: "Bob"
}),
utils.mkMembership({
room: roomId, mship: "join", user: userId, name: "Alice"
}),
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}
}),
utils.mkMembership({
room: roomId, mship: "join", user: userB, name: "Bob"
}),
utils.mkMembership({
room: roomId, mship: "join", user: userId, name: "Alice"
}),
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}]
}
}
}
};
beforeEach(function() {
@@ -75,6 +73,10 @@ describe("MatrixClient opts", function() {
});
});
afterEach(function() {
client.stopClient();
});
it("should be able to send messages", function(done) {
var eventId = "$flibble:wibble";
httpBackend.when("PUT", "/txn1").respond(200, {
@@ -101,13 +103,13 @@ describe("MatrixClient opts", function() {
);
});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" });
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush("/pushrules", 1).then(function() {
return httpBackend.flush("/initialSync", 1);
return httpBackend.flush("/filter", 1);
}).then(function() {
return httpBackend.flush("/events", 1);
return httpBackend.flush("/sync", 1);
}).done(function() {
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes
+55 -1
View File
@@ -2,22 +2,30 @@
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
var EventStatus = sdk.EventStatus;
describe("MatrixClient retrying", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var scheduler;
var userId = "@alice:localhost";
var accessToken = "aseukfgwef";
var roomId = "!room:here";
var room;
beforeEach(function() {
utils.beforeEach(this);
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
scheduler = new sdk.MatrixScheduler();
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken
accessToken: accessToken,
scheduler: scheduler,
});
room = new sdk.Room(roomId);
client.store.storeRoom(room);
});
afterEach(function() {
@@ -40,6 +48,52 @@ describe("MatrixClient retrying", function() {
});
it("should mark events as EventStatus.CANCELLED when cancelled", function(done) {
// send a couple of events; the second will be queued
var ev1, ev2;
client.sendMessage(roomId, "m1").then(function(ev) {
expect(ev).toEqual(ev1);
});
client.sendMessage(roomId, "m2").then(function(ev) {
expect(ev).toEqual(ev2);
});
// both events should be in the timeline at this point
var tl = room.getLiveTimeline().getEvents();
expect(tl.length).toEqual(2);
ev1 = tl[0];
ev2 = tl[1];
expect(ev1.status).toEqual(EventStatus.SENDING);
expect(ev2.status).toEqual(EventStatus.SENDING);
// the first message should get sent, and the second should get queued
httpBackend.when("PUT", "/send/m.room.message/").check(function(rq) {
// ev2 should now have been queued
expect(ev2.status).toEqual(EventStatus.QUEUED);
// now we can cancel the second and check everything looks sane
client.cancelPendingEvent(ev2);
expect(ev2.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(1);
// shouldn't be able to cancel the first message yet
expect(function() { client.cancelPendingEvent(ev1); })
.toThrow();
}).respond(400); // fail the first message
httpBackend.flush().then(function() {
expect(ev1.status).toEqual(EventStatus.NOT_SENT);
expect(tl.length).toEqual(1);
// cancel the first message
client.cancelPendingEvent(ev1);
expect(ev1.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(0);
}).catch(utils.failTest).done(done);
});
describe("resending", function() {
xit("should be able to resend a NOT_SENT event", function() {
+280 -130
View File
@@ -12,45 +12,94 @@ describe("MatrixClient room timelines", function() {
var accessToken = "aseukfgwef";
var roomId = "!foo:bar";
var otherUserId = "@bob:localhost";
var eventData;
var initialSync = {
end: "s_5_3",
presence: [],
rooms: [{
membership: "join",
room_id: roomId,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello"
})
]
},
state: [
utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId,
content: {
name: "Old room name"
var USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName
});
var ROOM_NAME_EVENT = utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId,
content: {
name: "Old room name"
}
});
var NEXT_SYNC_DATA;
var SYNC_DATA = {
next_batch: "s_5_3",
rooms: {
join: {
"!foo:bar": { // roomId
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello"
})
],
prev_batch: "f_1_1"
},
state: {
events: [
ROOM_NAME_EVENT,
utils.mkMembership({
room: roomId, mship: "join",
user: otherUserId, name: "Bob"
}),
USER_MEMBERSHIP_EVENT,
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}
}),
utils.mkMembership({
room: roomId, mship: "join", user: otherUserId, name: "Bob"
}),
utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName
}),
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}]
}
}
}
};
function setNextSyncData(events) {
events = events || [];
NEXT_SYNC_DATA = {
next_batch: "n",
presence: { events: [] },
rooms: {
invite: {},
join: {
"!foo:bar": {
timeline: { events: [] },
state: { events: [] },
ephemeral: { events: [] }
}
},
leave: {}
}
};
events.forEach(function(e) {
if (e.room_id !== roomId) {
throw new Error("setNextSyncData only works with one room id");
}
if (e.state_key) {
if (e.__prev_event === undefined) {
throw new Error(
"setNextSyncData needs the prev state set to '__prev_event' " +
"for " + e.type
);
}
if (e.__prev_event !== null) {
// push the previous state for this event type
NEXT_SYNC_DATA.rooms.join[roomId].state.events.push(e.__prev_event);
}
// push the current
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
}
else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
}
else {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
}
});
}
beforeEach(function(done) {
utils.beforeEach(this);
httpBackend = new HttpBackend();
@@ -58,31 +107,34 @@ describe("MatrixClient room timelines", function() {
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken
accessToken: accessToken,
// these tests should work with or without timelineSupport
timelineSupport: true,
});
eventData = {
chunk: [],
end: "end_",
start: "start_"
};
setNextSyncData();
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, function() {
return eventData;
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
client.startClient();
httpBackend.flush("/pushrules").done(done);
httpBackend.flush("/pushrules").then(function() {
return httpBackend.flush("/filter");
}).done(done);
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("local echo events", function() {
it("should be added immediately after calling MatrixClient.sendEvent " +
"with EventStatus.SENDING and the right event.sender", function(done) {
client.on("syncComplete", function() {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
@@ -96,11 +148,11 @@ describe("MatrixClient room timelines", function() {
expect(member.userId).toEqual(userId);
expect(member.name).toEqual(userName);
httpBackend.flush("/events", 1).done(function() {
httpBackend.flush("/sync", 1).done(function() {
done();
});
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should be updated correctly when the send request finishes " +
@@ -109,26 +161,28 @@ describe("MatrixClient room timelines", function() {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId
});
eventData.chunk = [
utils.mkMessage({
body: "I am a fish", user: userId, room: roomId
})
];
eventData.chunk[0].event_id = eventId;
client.on("syncComplete", function() {
var ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId
});
ev.event_id = eventId;
ev.unsigned = {transaction_id: "txn1"};
setNextSyncData([ev]);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
client.sendTextMessage(roomId, "I am a fish", "txn1").done(
function() {
expect(room.timeline[1].getId()).toEqual(eventId);
httpBackend.flush("/events", 1).done(function() {
httpBackend.flush("/sync", 1).done(function() {
expect(room.timeline[1].getId()).toEqual(eventId);
done();
});
});
httpBackend.flush("/txn1", 1);
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should be updated correctly when the send request finishes " +
@@ -137,19 +191,20 @@ describe("MatrixClient room timelines", function() {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId
});
eventData.chunk = [
utils.mkMessage({
body: "I am a fish", user: userId, room: roomId
})
];
eventData.chunk[0].event_id = eventId;
client.on("syncComplete", function() {
var ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId
});
ev.event_id = eventId;
ev.unsigned = {transaction_id: "txn1"};
setNextSyncData([ev]);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
var promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
httpBackend.flush("/events", 1).done(function() {
// expect 3rd msg, it doesn't know this is the request is just did
expect(room.timeline.length).toEqual(3);
httpBackend.flush("/sync", 1).done(function() {
expect(room.timeline.length).toEqual(2);
httpBackend.flush("/txn1", 1);
promise.done(function() {
expect(room.timeline.length).toEqual(2);
@@ -159,7 +214,7 @@ describe("MatrixClient room timelines", function() {
});
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
});
@@ -180,7 +235,8 @@ describe("MatrixClient room timelines", function() {
it("should set Room.oldState.paginationToken to null at the start" +
" of the timeline.", function(done) {
client.on("syncComplete", function() {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
@@ -191,13 +247,29 @@ describe("MatrixClient room timelines", function() {
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1);
httpBackend.flush("/sync", 1);
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should set the right event.sender values", function(done) {
// make an m.room.member event with prev_content
// We're aiming for an eventual timeline of:
//
// 'Old Alice' joined the room
// <Old Alice> I'm old alice
// @alice:localhost changed their name from 'Old Alice' to 'Alice'
// <Alice> I'm alice
// ------^ /messages results above this point, /sync result below
// <Bob> hello
// make an m.room.member event for alice's join
var joinMshipEvent = utils.mkMembership({
mship: "join", user: userId, room: roomId, name: "Old Alice",
url: null
});
// make an m.room.member event with prev_content for alice's nick
// change
var oldMshipEvent = utils.mkMembership({
mship: "join", user: userId, room: roomId, name: userName,
url: "mxc://some/url"
@@ -208,7 +280,8 @@ describe("MatrixClient room timelines", function() {
membership: "join"
};
// set the list of events to return on scrollback
// set the list of events to return on scrollback (/messages)
// N.B. synapse returns /messages in reverse chronological order
sbEvents = [
utils.mkMessage({
user: userId, room: roomId, msg: "I'm alice"
@@ -216,26 +289,31 @@ describe("MatrixClient room timelines", function() {
oldMshipEvent,
utils.mkMessage({
user: userId, room: roomId, msg: "I'm old alice"
})
}),
joinMshipEvent,
];
client.on("syncComplete", function() {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
// sync response
expect(room.timeline.length).toEqual(1);
client.scrollback(room).done(function() {
expect(room.timeline.length).toEqual(4);
var oldMsg = room.timeline[0];
expect(room.timeline.length).toEqual(5);
var joinMsg = room.timeline[0];
expect(joinMsg.sender.name).toEqual("Old Alice");
var oldMsg = room.timeline[1];
expect(oldMsg.sender.name).toEqual("Old Alice");
var newMsg = room.timeline[2];
var newMsg = room.timeline[3];
expect(newMsg.sender.name).toEqual(userName);
done();
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1);
httpBackend.flush("/sync", 1);
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should add it them to the right place in the timeline", function(done) {
@@ -249,7 +327,8 @@ describe("MatrixClient room timelines", function() {
})
];
client.on("syncComplete", function() {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
@@ -261,9 +340,9 @@ describe("MatrixClient room timelines", function() {
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1);
httpBackend.flush("/sync", 1);
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should use 'end' as the next pagination token", function(done) {
@@ -274,7 +353,8 @@ describe("MatrixClient room timelines", function() {
})
];
client.on("syncComplete", function() {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
expect(room.oldState.paginationToken).toBeDefined();
@@ -282,109 +362,117 @@ describe("MatrixClient room timelines", function() {
expect(room.oldState.paginationToken).toEqual(sbEndTok);
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1).done(function() {
done();
});
httpBackend.flush("/sync", 1);
httpBackend.flush("/messages", 1).done(function() {
done();
});
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
});
describe("new events", function() {
it("should be added to the right place in the timeline", function(done) {
eventData.chunk = [
var eventData = [
utils.mkMessage({user: userId, room: roomId}),
utils.mkMessage({user: userId, room: roomId})
];
client.on("syncComplete", function() {
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
var index = 0;
client.on("Room.timeline", function(event, rm, toStart) {
expect(toStart).toBe(false);
expect(rm).toEqual(room);
expect(event.event).toEqual(eventData.chunk[index]);
expect(event.event).toEqual(eventData[index]);
index += 1;
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
expect(index).toEqual(2);
expect(room.timeline[room.timeline.length - 1].event).toEqual(
eventData.chunk[1]
expect(room.timeline.length).toEqual(3);
expect(room.timeline[2].event).toEqual(
eventData[1]
);
expect(room.timeline[room.timeline.length - 2].event).toEqual(
eventData.chunk[0]
expect(room.timeline[1].event).toEqual(
eventData[0]
);
done();
});
}).catch(utils.failTest).done(done);
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should set the right event.sender values", function(done) {
eventData.chunk = [
var eventData = [
utils.mkMessage({user: userId, room: roomId}),
utils.mkMembership({
user: userId, room: roomId, mship: "join", name: "New Name"
}),
utils.mkMessage({user: userId, room: roomId})
];
client.on("syncComplete", function() {
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
httpBackend.flush("/events", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
var preNameEvent = room.timeline[room.timeline.length - 3];
var postNameEvent = room.timeline[room.timeline.length - 1];
expect(preNameEvent.sender.name).toEqual(userName);
expect(postNameEvent.sender.name).toEqual("New Name");
done();
});
}).catch(utils.failTest).done(done);
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should set the right room.name", function(done) {
eventData.chunk = [
utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 2"
}
})
];
client.on("syncComplete", function() {
var secondRoomNameEvent = utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 2"
}
});
secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
setNextSyncData([secondRoomNameEvent]);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
var nameEmitCount = 0;
client.on("Room.name", function(rm) {
nameEmitCount += 1;
});
httpBackend.flush("/events", 1).done(function() {
httpBackend.flush("/sync", 1).done(function() {
expect(nameEmitCount).toEqual(1);
expect(room.name).toEqual("Room 2");
// do another round
eventData.chunk = [
utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 3"
}
})
];
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.flush("/events", 1).done(function() {
var thirdRoomNameEvent = utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 3"
}
});
thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
setNextSyncData([thirdRoomNameEvent]);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend.flush("/sync", 1).done(function() {
expect(nameEmitCount).toEqual(2);
expect(room.name).toEqual("Room 3");
done();
});
});
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should set the right room members", function(done) {
var userC = "@cee:bar";
var userD = "@dee:bar";
eventData.chunk = [
var eventData = [
utils.mkMembership({
user: userC, room: roomId, mship: "join", name: "C"
}),
@@ -392,9 +480,14 @@ describe("MatrixClient room timelines", function() {
user: userC, room: roomId, mship: "invite", skey: userD
})
];
client.on("syncComplete", function() {
eventData[0].__prev_event = null;
eventData[1].__prev_event = null;
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
httpBackend.flush("/events", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC).name).toEqual("C");
expect(room.currentState.getMember(userC).membership).toEqual(
@@ -404,10 +497,67 @@ describe("MatrixClient room timelines", function() {
expect(room.currentState.getMember(userD).membership).toEqual(
"invite"
);
}).catch(utils.failTest).done(done);
});
httpBackend.flush("/sync", 1);
});
});
describe("gappy sync", function() {
it("should copy the last known state to the new timeline", function(done) {
var eventData = [
utils.mkMessage({user: userId, room: roomId}),
];
setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).done(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(eventData[0]);
expect(room.currentState.getMembers().length).toEqual(2);
expect(room.currentState.getMember(userId).name).toEqual(userName);
expect(room.currentState.getMember(userId).membership).toEqual(
"join"
);
expect(room.currentState.getMember(otherUserId).name).toEqual("Bob");
expect(room.currentState.getMember(otherUserId).membership).toEqual(
"join"
);
done();
});
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should emit a 'Room.timelineReset' event", function(done) {
var eventData = [
utils.mkMessage({user: userId, room: roomId}),
];
setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
var emitCount = 0;
client.on("Room.timelineReset", function(emitRoom) {
expect(emitRoom).toEqual(room);
emitCount++;
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).done(function() {
expect(emitCount).toEqual(1);
done();
});
});
httpBackend.flush("/sync", 1);
});
});
});
+552 -145
View File
@@ -2,6 +2,8 @@
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
var MatrixEvent = sdk.MatrixEvent;
var EventTimeline = sdk.EventTimeline;
describe("MatrixClient syncing", function() {
var baseUrl = "http://localhost.or.something";
@@ -9,6 +11,11 @@ describe("MatrixClient syncing", function() {
var selfUserId = "@alice:localhost";
var selfAccessToken = "aseukfgwef";
var otherUserId = "@bob:localhost";
var userA = "@alice:bar";
var userB = "@bob:bar";
var userC = "@claire:bar";
var roomOne = "!foo:localhost";
var roomTwo = "!bar:localhost";
beforeEach(function() {
utils.beforeEach(this);
@@ -20,27 +27,23 @@ describe("MatrixClient syncing", function() {
accessToken: selfAccessToken
});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("startClient", function() {
var initialSync = {
end: "s_5_3",
presence: [],
rooms: []
};
var eventData = {
start: "s_5_3",
end: "e_6_7",
chunk: []
var syncData = {
next_batch: "batch_token",
rooms: {},
presence: {}
};
it("should start with /initialSync then move onto /events.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
it("should /sync after /pushrules and /filter.", function(done) {
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
@@ -49,12 +52,12 @@ describe("MatrixClient syncing", function() {
});
});
it("should pass the 'end' token from /initialSync to the from= param " +
" of /events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").check(function(req) {
expect(req.queryParams.from).toEqual(initialSync.end);
}).respond(200, eventData);
it("should pass the 'next_batch' token from /sync to the since= param " +
" of the next /sync", function(done) {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").check(function(req) {
expect(req.queryParams.since).toEqual(syncData.next_batch);
}).respond(200, syncData);
client.startClient();
@@ -64,81 +67,32 @@ describe("MatrixClient syncing", function() {
});
});
describe("users", function() {
var userA = "@alice:bar";
var userB = "@bob:bar";
var userC = "@claire:bar";
var initialSync = {
end: "s_5_3",
presence: [
utils.mkPresence({
user: userA, presence: "online"
}),
utils.mkPresence({
user: userB, presence: "unavailable"
})
],
rooms: []
};
var eventData = {
start: "s_5_3",
end: "e_6_7",
chunk: [
// existing user change
utils.mkPresence({
user: userA, presence: "offline"
}),
// new user C
utils.mkPresence({
user: userC, presence: "online"
})
]
describe("resolving invites to profile info", function() {
var syncData = {
next_batch: "s_5_3",
presence: {
events: []
},
rooms: {
join: {
}
}
};
it("should create users for presence events from /initialSync and /events",
function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
client.startClient();
httpBackend.flush().done(function() {
expect(client.getUser(userA).presence).toEqual("offline");
expect(client.getUser(userB).presence).toEqual("unavailable");
expect(client.getUser(userC).presence).toEqual("online");
done();
});
});
});
describe("room state", function() {
var roomOne = "!foo:localhost";
var roomTwo = "!bar:localhost";
var msgText = "some text here";
var otherDisplayName = "Bob Smith";
var initialSync = {
end: "s_5_3",
presence: [],
rooms: [
{
membership: "join",
room_id: roomOne,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
})
]
},
state: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name"
}
}),
beforeEach(function() {
syncData.presence.events = [];
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
})
]
},
state: {
events: [
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId
}),
@@ -152,72 +106,271 @@ describe("MatrixClient syncing", function() {
}
})
]
},
{
membership: "join",
room_id: roomTwo,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hiii"
})
]
},
state: [
utils.mkMembership({
room: roomTwo, mship: "join", user: otherUserId,
name: otherDisplayName
}),
utils.mkMembership({
room: roomTwo, mship: "join", user: selfUserId
}),
utils.mkEvent({
type: "m.room.create", room: roomTwo, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
]
};
var eventData = {
start: "s_5_3",
end: "e_6_7",
chunk: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: selfUserId,
content: { name: "A new room name" }
}),
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: msgText
}),
utils.mkEvent({
type: "m.typing", room: roomTwo,
content: { user_ids: [otherUserId] }
};
});
it("should resolve incoming invites from /sync", function(done) {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
]
);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/profile/" + encodeURIComponent(userC)).respond(
200, {
avatar_url: "mxc://flibble/wibble",
displayname: "The Boss"
}
);
client.startClient({
resolveInvitesToProfiles: true
});
httpBackend.flush().done(function() {
var member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual("The Boss");
expect(
member.getAvatarUrl("home.server.url", null, null, null, false)
).toBeDefined();
done();
});
});
it("should use cached values from m.presence wherever possible", function(done) {
syncData.presence.events = [
utils.mkPresence({
user: userC, presence: "online", name: "The Ghost"
}),
];
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
);
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient({
resolveInvitesToProfiles: true
});
httpBackend.flush().done(function() {
var member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual("The Ghost");
done();
});
});
it("should result in events on the room member firing", function(done) {
syncData.presence.events = [
utils.mkPresence({
user: userC, presence: "online", name: "The Ghost"
})
];
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
);
httpBackend.when("GET", "/sync").respond(200, syncData);
var latestFiredName = null;
client.on("RoomMember.name", function(event, m) {
if (m.userId === userC && m.roomId === roomOne) {
latestFiredName = m.name;
}
});
client.startClient({
resolveInvitesToProfiles: true
});
httpBackend.flush().done(function() {
expect(latestFiredName).toEqual("The Ghost");
done();
});
});
it("should no-op if resolveInvitesToProfiles is not set", function(done) {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
);
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush().done(function() {
var member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual(userC);
expect(
member.getAvatarUrl("home.server.url", null, null, null, false)
).toBeNull();
done();
});
});
});
describe("users", function() {
var syncData = {
next_batch: "nb",
presence: {
events: [
utils.mkPresence({
user: userA, presence: "online"
}),
utils.mkPresence({
user: userB, presence: "unavailable"
})
]
}
};
it("should create users for presence events from /sync",
function(done) {
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush().done(function() {
expect(client.getUser(userA).presence).toEqual("online");
expect(client.getUser(userB).presence).toEqual("unavailable");
done();
});
});
});
describe("room state", function() {
var msgText = "some text here";
var otherDisplayName = "Bob Smith";
var syncData = {
rooms: {
join: {
}
}
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
})
]
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name"
}
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
};
syncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hiii"
})
]
},
state: {
events: [
utils.mkMembership({
room: roomTwo, mship: "join", user: otherUserId,
name: otherDisplayName
}),
utils.mkMembership({
room: roomTwo, mship: "join", user: selfUserId
}),
utils.mkEvent({
type: "m.room.create", room: roomTwo, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
};
var nextSyncData = {
rooms: {
join: {
}
}
};
nextSyncData.rooms.join[roomOne] = {
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: selfUserId,
content: { name: "A new room name" }
})
]
}
};
nextSyncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: msgText
})
]
},
ephemeral: {
events: [
utils.mkEvent({
type: "m.typing", room: roomTwo,
content: { user_ids: [otherUserId] }
})
]
}
};
it("should continually recalculate the right room name.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomOne);
// should have clobbered the name to the one from /events
expect(room.name).toEqual(eventData.chunk[0].content.name);
expect(room.name).toEqual(
nextSyncData.rooms.join[roomOne].state.events[0].content.name
);
done();
});
});
it("should store the right events in the timeline.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
@@ -231,8 +384,8 @@ describe("MatrixClient syncing", function() {
});
it("should set the right room name.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
httpBackend.flush().done(function() {
@@ -244,8 +397,8 @@ describe("MatrixClient syncing", function() {
});
it("should set the right user's typing flag.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
@@ -270,6 +423,182 @@ describe("MatrixClient syncing", function() {
});
});
describe("timeline", function() {
beforeEach(function() {
var syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
}),
],
prev_batch: "pagTok",
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush();
});
it("should set the back-pagination token on new rooms", function(done) {
var syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "roomtwo"
}),
],
prev_batch: "roomtwotok",
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.flush().then(function() {
var room = client.getRoom(roomTwo);
var tok = room.getLiveTimeline()
.getPaginationToken(EventTimeline.BACKWARDS);
expect(tok).toEqual("roomtwotok");
done();
}).catch(utils.failTest).done();
});
it("should set the back-pagination token on gappy syncs", function(done) {
var syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world"
}),
],
limited: true,
prev_batch: "newerTok",
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
var resetCallCount = 0;
// the token should be set *before* timelineReset is emitted
client.on("Room.timelineReset", function(room) {
resetCallCount++;
var tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(0);
var tok = tl.getPaginationToken(EventTimeline.BACKWARDS);
expect(tok).toEqual("newerTok");
});
httpBackend.flush().then(function() {
var room = client.getRoom(roomOne);
var tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(1);
expect(resetCallCount).toEqual(1);
done();
}).catch(utils.failTest).done();
});
});
describe("receipts", function() {
var syncData = {
rooms: {
join: {
}
}
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
}),
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world"
})
]
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name"
}
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
};
beforeEach(function() {
syncData.rooms.join[roomOne].ephemeral = {
events: []
};
});
it("should sync receipts from /sync.", function(done) {
var ackEvent = syncData.rooms.join[roomOne].timeline.events[0];
var receipt = {};
receipt[ackEvent.event_id] = {
"m.read": {}
};
receipt[ackEvent.event_id]["m.read"][userC] = {
ts: 176592842636
};
syncData.rooms.join[roomOne].ephemeral.events = [{
content: receipt,
room_id: roomOne,
type: "m.receipt"
}];
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomOne);
expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{
type: "m.read",
userId: userC,
data: {
ts: 176592842636
}
}]);
done();
});
});
});
describe("of a room", function() {
xit("should sync when a join event (which changes state) for the user" +
" arrives down the event stream (e.g. join from another device)", function() {
@@ -280,4 +609,82 @@ describe("MatrixClient syncing", function() {
});
});
describe("syncLeftRooms", function() {
beforeEach(function(done) {
client.startClient();
httpBackend.flush().then(function() {
// the /sync call from syncLeftRooms ends up in the request
// queue behind the call from the running client; add a response
// to flush the client's one out.
httpBackend.when("GET", "/sync").respond(200, {});
done();
});
});
it("should create and use an appropriate filter", function(done) {
httpBackend.when("POST", "/filter").check(function(req) {
expect(req.data).toEqual({
room: { timeline: {limit: 1},
include_leave: true }});
}).respond(200, { filter_id: "another_id" });
httpBackend.when("GET", "/sync").check(function(req) {
expect(req.queryParams.filter).toEqual("another_id");
done();
}).respond(200, {});
client.syncLeftRooms();
// first flush the filter request; this will make syncLeftRooms
// make its /sync call
httpBackend.flush("/filter").then(function() {
// flush the syncs
return httpBackend.flush();
}).catch(utils.failTest);
});
it("should set the back-pagination token on left rooms", function(done) {
var syncData = {
next_batch: "batch_token",
rooms: {
leave: {}
},
};
syncData.rooms.leave[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hello"
}),
],
prev_batch: "pagTok",
},
};
httpBackend.when("POST", "/filter").respond(200, {
filter_id: "another_id"
});
httpBackend.when("GET", "/sync").respond(200, syncData);
client.syncLeftRooms().then(function() {
var room = client.getRoom(roomTwo);
var tok = room.getLiveTimeline().getPaginationToken(
EventTimeline.BACKWARDS);
expect(tok).toEqual("pagTok");
done();
}).catch(utils.failTest).done();
// first flush the filter request; this will make syncLeftRooms
// make its /sync call
httpBackend.flush("/filter").then(function() {
return httpBackend.flush();
}).catch(utils.failTest);
});
});
});
+893
View File
@@ -0,0 +1,893 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
try {
var Olm = require('olm');
} catch (e) {}
var anotherjson = require('another-json');
var q = require('q');
var sdk = require('../..');
var utils = require('../../lib/utils');
var test_utils = require('../test-utils');
var MockHttpBackend = require('../mock-request');
var ROOM_ID = "!room:id";
/**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
*
* @constructor
* @param {string} userId
* @param {string} deviceId
* @param {string} accessToken
*/
function TestClient(userId, deviceId, accessToken) {
this.userId = userId;
this.deviceId = deviceId;
this.storage = new sdk.WebStorageSessionStore(new test_utils.MockStorageApi());
this.httpBackend = new MockHttpBackend();
this.client = sdk.createClient({
baseUrl: "http://test.server",
userId: userId,
accessToken: accessToken,
deviceId: deviceId,
sessionStore: this.storage,
request: this.httpBackend.requestFn,
});
this.deviceKeys = null;
this.oneTimeKeys = [];
}
/**
* start the client, and wait for it to initialise.
*
* @param {object?} deviceQueryResponse the list of our existing devices to return from
* the /query request. Defaults to empty device list
* @return {Promise}
*/
TestClient.prototype.start = function(existingDevices) {
var self = this;
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.httpBackend.when('POST', '/keys/query').respond(200, function(path, content) {
expect(content.device_keys[self.userId]).toEqual({});
var res = existingDevices;
if (!res) {
res = { device_keys: {} };
res.device_keys[self.userId] = {};
}
return res;
});
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
expect(content.one_time_keys).not.toBeDefined();
expect(content.device_keys).toBeDefined();
self.deviceKeys = content.device_keys;
return {one_time_key_counts: {signed_curve25519: 0}};
});
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
expect(content.device_keys).not.toBeDefined();
expect(content.one_time_keys).toBeDefined();
expect(content.one_time_keys).not.toEqual({});
self.oneTimeKeys = content.one_time_keys;
return {one_time_key_counts: {
signed_curve25519: utils.keys(self.oneTimeKeys).length
}};
});
this.client.startClient();
return this.httpBackend.flush();
};
/**
* stop the client
*/
TestClient.prototype.stop = function() {
this.client.stopClient();
};
/**
* get the uploaded curve25519 device key
*
* @return {string} base64 device key
*/
TestClient.prototype.getDeviceKey = function() {
var key_id = 'curve25519:' + this.deviceId;
return this.deviceKeys.keys[key_id];
};
/**
* get the uploaded ed25519 device key
*
* @return {string} base64 device key
*/
TestClient.prototype.getSigningKey = function() {
var key_id = 'ed25519:' + this.deviceId;
return this.deviceKeys.keys[key_id];
};
/**
* start an Olm session with a given recipient
*
* @param {Olm.Account} olmAccount
* @param {TestClient} recipientTestClient
* @return {Olm.Session}
*/
function createOlmSession(olmAccount, recipientTestClient) {
var otk_id = utils.keys(recipientTestClient.oneTimeKeys)[0];
var otk = recipientTestClient.oneTimeKeys[otk_id];
var session = new Olm.Session();
session.create_outbound(
olmAccount, recipientTestClient.getDeviceKey(), otk.key
);
return session;
}
/**
* encrypt an event with olm
*
* @param {object} opts
* @param {string=} opts.sender
* @param {string} opts.senderKey
* @param {Olm.Session} opts.p2pSession
* @param {TestClient} opts.recipient
* @param {object=} opts.plaincontent
* @param {string=} opts.plaintype
*
* @return {object} event
*/
function encryptOlmEvent(opts) {
expect(opts.senderKey).toBeDefined();
expect(opts.p2pSession).toBeDefined();
expect(opts.recipient).toBeDefined();
var plaintext = {
content: opts.plaincontent || {},
recipient: opts.recipient.userId,
recipient_keys: {
ed25519: opts.recipient.getSigningKey(),
},
sender: opts.sender || '@bob:xyz',
type: opts.plaintype || 'm.test',
};
var event = {
content: {
algorithm: 'm.olm.v1.curve25519-aes-sha2',
ciphertext: {},
sender_key: opts.senderKey,
},
sender: opts.sender || '@bob:xyz',
type: 'm.room.encrypted',
};
event.content.ciphertext[opts.recipient.getDeviceKey()] =
opts.p2pSession.encrypt(JSON.stringify(plaintext));
return event;
}
/**
* encrypt an event with megolm
*
* @param {object} opts
* @param {string} opts.senderKey
* @param {Olm.OutboundGroupSession} opts.groupSession
* @param {object=} opts.plaintext
* @param {string=} opts.room_id
*
* @return {object} event
*/
function encryptMegolmEvent(opts) {
expect(opts.senderKey).toBeDefined();
expect(opts.groupSession).toBeDefined();
var plaintext = opts.plaintext || {};
if (!plaintext.content) {
plaintext.content = {
body: '42',
msgtype: "m.text",
};
}
if (!plaintext.type) {
plaintext.type = "m.room.message";
}
if (!plaintext.room_id) {
expect(opts.room_id).toBeDefined();
plaintext.room_id = opts.room_id;
}
return {
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: opts.groupSession.encrypt(JSON.stringify(plaintext)),
device_id: "testDevice",
sender_key: opts.senderKey,
session_id: opts.groupSession.session_id(),
},
type: "m.room.encrypted",
};
}
/**
* build an encrypted room_key event to share a group session
*
* @param {object} opts
* @param {string} opts.senderKey
* @param {TestClient} opts.recipient
* @param {Olm.Session} opts.p2pSession
* @param {Olm.OutboundGroupSession} opts.groupSession
* @param {string=} opts.room_id
*
* @return {object} event
*/
function encryptGroupSessionKey(opts) {
return encryptOlmEvent({
senderKey: opts.senderKey,
recipient: opts.recipient,
p2pSession: opts.p2pSession,
plaincontent: {
algorithm: 'm.megolm.v1.aes-sha2',
room_id: opts.room_id,
session_id: opts.groupSession.session_id(),
session_key: opts.groupSession.session_key(),
},
plaintype: 'm.room_key',
});
}
/**
* get a /sync response which contains a single room (ROOM_ID),
* with the members given
*
* @param {string[]} roomMembers
*
* @return {object} event
*/
function getSyncResponse(roomMembers) {
var roomResponse = {
state: {
events: [
test_utils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
}),
],
}
};
for (var i = 0; i < roomMembers.length; i++) {
roomResponse.state.events.push(
test_utils.mkMembership({
mship: 'join',
sender: roomMembers[i],
})
);
}
var syncResponse = {
next_batch: 1,
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = roomResponse;
return syncResponse;
}
describe("megolm", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
var testOlmAccount;
var testSenderKey;
var aliceTestClient;
/**
* Get the device keys for testOlmAccount in a format suitable for a
* response to /keys/query
*/
function getTestKeysQueryResponse(userId) {
var testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
var testDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'DEVICE_ID',
keys: {
'curve25519:DEVICE_ID': testE2eKeys.curve25519,
'ed25519:DEVICE_ID': testE2eKeys.ed25519,
},
user_id: userId,
};
var j = anotherjson.stringify(testDeviceKeys);
var sig = testOlmAccount.sign(j);
testDeviceKeys.signatures = {};
testDeviceKeys.signatures[userId] = {
'ed25519:DEVICE_ID': sig,
};
var queryResponse = {
device_keys: {},
};
queryResponse.device_keys[userId] = {
'DEVICE_ID': testDeviceKeys,
};
return queryResponse;
}
/**
* Get a one-time key for testOlmAccount in a format suitable for a
* response to /keys/claim
*/
function getTestKeysClaimResponse(userId) {
testOlmAccount.generate_one_time_keys(1);
var testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys());
testOlmAccount.mark_keys_as_published();
var keyId = utils.keys(testOneTimeKeys.curve25519)[0];
var oneTimeKey = testOneTimeKeys.curve25519[keyId];
var keyResult = {
'key': oneTimeKey,
};
var j = anotherjson.stringify(keyResult);
var sig = testOlmAccount.sign(j);
keyResult.signatures = {};
keyResult.signatures[userId] = {
'ed25519:DEVICE_ID': sig,
};
var claimResponse = {one_time_keys: {}};
claimResponse.one_time_keys[userId] = {
'DEVICE_ID': {},
};
claimResponse.one_time_keys[userId].DEVICE_ID['signed_curve25519:' + keyId] =
keyResult;
return claimResponse;
}
beforeEach(function() {
test_utils.beforeEach(this);
aliceTestClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs"
);
testOlmAccount = new Olm.Account();
testOlmAccount.create();
var testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
testSenderKey = testE2eKeys.curve25519;
});
afterEach(function() {
aliceTestClient.stop();
});
it("Alice receives a megolm message", function(done) {
return aliceTestClient.start().then(function() {
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
var groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
var roomKeyEncrypted = encryptGroupSessionKey({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a message with the group session
var messageEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
});
// Alice gets both the events in a single sync
var syncResponse = {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted],
},
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
timeline: {
events: [messageEncrypted],
},
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.httpBackend.flush("/sync", 1);
}).then(function() {
var room = aliceTestClient.client.getRoom(ROOM_ID);
var event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42');
}).nodeify(done);
});
it("Alice gets a second room_key message", function(done) {
return aliceTestClient.start().then(function() {
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
var groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
var roomKeyEncrypted1 = encryptGroupSessionKey({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a message with the group session
var messageEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
});
// make a second room_key event now that we have advanced the group
// session.
var roomKeyEncrypted2 = encryptGroupSessionKey({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// on the first sync, send the best room key
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted1],
},
});
// on the second sync, send the advanced room key, along with the
// message. This simulates the situation where Alice has been sent a
// later copy of the room key and is reloading the client.
var syncResponse2 = {
next_batch: 2,
to_device: {
events: [roomKeyEncrypted2],
},
rooms: {
join: {},
},
};
syncResponse2.rooms.join[ROOM_ID] = {
timeline: {
events: [messageEncrypted],
},
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse2);
return aliceTestClient.httpBackend.flush("/sync", 2);
}).then(function() {
var room = aliceTestClient.client.getRoom(ROOM_ID);
var event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42');
}).nodeify(done);
});
it('Alice sends a megolm message', function(done) {
var p2pSession;
return aliceTestClient.start().then(function() {
var syncResponse = getSyncResponse(['@bob:xyz']);
// establish an olm session with alice
p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
var olmEvent = encryptOlmEvent({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
});
syncResponse.to_device = { events: [olmEvent] };
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1);
}).then(function() {
var inboundGroupSession;
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz')
);
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/'
).respond(200, function(path, content) {
var m = content.messages['@bob:xyz'].DEVICE_ID;
var ct = m.ciphertext[testSenderKey];
var decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
expect(decrypted.type).toEqual('m.room_key');
inboundGroupSession = new Olm.InboundGroupSession();
inboundGroupSession.create(decrypted.content.session_key);
return {};
});
aliceTestClient.httpBackend.when(
'PUT', '/send/'
).respond(200, function(path, content) {
var ct = content.ciphertext;
var r = inboundGroupSession.decrypt(ct);
console.log('Decrypted received megolm message', r);
expect(r.message_index).toEqual(0);
var decrypted = JSON.parse(r.plaintext);
expect(decrypted.type).toEqual('m.room.message');
expect(decrypted.content.body).toEqual('test');
return {
event_id: '$event_id',
};
});
return q.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
]);
}).nodeify(done);
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function(done) {
return aliceTestClient.start().then(function() {
var syncResponse = getSyncResponse(['@bob:xyz']);
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1);
}).then(function() {
console.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:xyz': {},
}
});
return q.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
console.log("Telling alice to send a megolm message");
aliceTestClient.httpBackend.when(
'PUT', '/send/'
).respond(200, {
event_id: '$event_id',
});
return q.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
]);
}).nodeify(done);
});
it("We shouldn't attempt to send to blocked devices", function(done) {
return aliceTestClient.start().then(function() {
var syncResponse = getSyncResponse(['@bob:xyz']);
// establish an olm session with alice
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
var olmEvent = encryptOlmEvent({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
});
syncResponse.to_device = { events: [olmEvent] };
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1);
}).then(function() {
console.log('Forcing alice to download our device keys');
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz')
);
return q.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
console.log('Telling alice to block our device');
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
console.log('Telling alice to send a megolm message');
aliceTestClient.httpBackend.when(
'PUT', '/send/'
).respond(200, {
event_id: '$event_id',
});
return q.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
]);
}).nodeify(done);
});
it("We should start a new megolm session when a device is blocked", function(done) {
var p2pSession;
var megolmSessionId;
return aliceTestClient.start().then(function() {
var syncResponse = getSyncResponse(['@bob:xyz']);
// establish an olm session with alice
p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
var olmEvent = encryptOlmEvent({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
});
syncResponse.to_device = { events: [olmEvent] };
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1);
}).then(function() {
console.log('Telling alice to send a megolm message');
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz')
);
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/'
).respond(200, function(path, content) {
console.log('sendToDevice: ', content);
var m = content.messages['@bob:xyz'].DEVICE_ID;
var ct = m.ciphertext[testSenderKey];
expect(ct.type).toEqual(1); // normal message
var decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
console.log('decrypted sendToDevice:', decrypted);
expect(decrypted.type).toEqual('m.room_key');
megolmSessionId = decrypted.content.session_id;
return {};
});
aliceTestClient.httpBackend.when(
'PUT', '/send/'
).respond(200, function(path, content) {
console.log('/send:', content);
expect(content.session_id).toEqual(megolmSessionId);
return {
event_id: '$event_id',
};
});
return q.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
]);
}).then(function() {
console.log('Telling alice to block our device');
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
console.log('Telling alice to send another megolm message');
aliceTestClient.httpBackend.when(
'PUT', '/send/'
).respond(200, function(path, content) {
console.log('/send:', content);
expect(content.session_id).not.toEqual(megolmSessionId);
return {
event_id: '$event_id',
};
});
return q.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),
aliceTestClient.httpBackend.flush(),
]);
}).nodeify(done);
});
// https://github.com/vector-im/riot-web/issues/2676
it("Alice should send to her other devices", function(done) {
// for this test, we make the testOlmAccount be another of Alice's devices.
// it ought to get include in messages Alice sends.
var p2pSession;
var inboundGroupSession;
var decrypted;
return aliceTestClient.start(
getTestKeysQueryResponse(aliceTestClient.userId)
).then(function() {
// an encrypted room with just alice
var syncResponse = {
next_batch: 1,
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
state: {
events: [
test_utils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
}),
test_utils.mkMembership({
mship: 'join',
sender: aliceTestClient.userId,
}),
],
},
};
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush();
}).then(function() {
aliceTestClient.httpBackend.when('POST', '/keys/claim').respond(
200, function(path, content)
{
expect(content.one_time_keys[aliceTestClient.userId].DEVICE_ID)
.toEqual("signed_curve25519");
return getTestKeysClaimResponse(aliceTestClient.userId);
});
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/'
).respond(200, function(path, content) {
console.log("sendToDevice: ", content);
var m = content.messages[aliceTestClient.userId].DEVICE_ID;
var ct = m.ciphertext[testSenderKey];
expect(ct.type).toEqual(0); // pre-key message
p2pSession = new Olm.Session();
p2pSession.create_inbound(testOlmAccount, ct.body);
var decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
expect(decrypted.type).toEqual('m.room_key');
inboundGroupSession = new Olm.InboundGroupSession();
inboundGroupSession.create(decrypted.content.session_key);
return {};
});
aliceTestClient.httpBackend.when(
'PUT', '/send/'
).respond(200, function(path, content) {
var ct = content.ciphertext;
var r = inboundGroupSession.decrypt(ct);
console.log('Decrypted received megolm message', r);
decrypted = JSON.parse(r.plaintext);
return {
event_id: '$event_id',
};
});
return q.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
]);
}).then(function() {
expect(decrypted.type).toEqual('m.room.message');
expect(decrypted.content.body).toEqual('test');
}).nodeify(done);
});
it('Alice should wait for device list to complete when sending a megolm message',
function(done) {
var p2pSession;
var inboundGroupSession;
var downloadPromise;
var sendPromise;
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/'
).respond(200, function(path, content) {
var m = content.messages['@bob:xyz'].DEVICE_ID;
var ct = m.ciphertext[testSenderKey];
var decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
expect(decrypted.type).toEqual('m.room_key');
inboundGroupSession = new Olm.InboundGroupSession();
inboundGroupSession.create(decrypted.content.session_key);
return {};
});
aliceTestClient.httpBackend.when(
'PUT', '/send/'
).respond(200, function(path, content) {
var ct = content.ciphertext;
var r = inboundGroupSession.decrypt(ct);
console.log('Decrypted received megolm message', r);
expect(r.message_index).toEqual(0);
var decrypted = JSON.parse(r.plaintext);
expect(decrypted.type).toEqual('m.room.message');
expect(decrypted.content.body).toEqual('test');
return {
event_id: '$event_id',
};
});
return aliceTestClient.start().then(function() {
var syncResponse = getSyncResponse(['@bob:xyz']);
// establish an olm session with alice
p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
var olmEvent = encryptOlmEvent({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
});
syncResponse.to_device = { events: [olmEvent] };
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1);
}).then(function() {
console.log('Forcing alice to download our device keys');
// this will block
downloadPromise = aliceTestClient.client.downloadKeys(['@bob:xyz']);
}).then(function() {
// so will this.
sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, 'test');
}).then(function() {
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, getTestKeysQueryResponse('@bob:xyz')
);
return aliceTestClient.httpBackend.flush();
}).then(function() {
return q.all([downloadPromise, sendPromise]);
}).nodeify(done);
});
});
+77 -8
View File
@@ -11,9 +11,23 @@ function HttpBackend() {
var self = this;
// the request function dependency that the SDK needs.
this.requestFn = function(opts, callback) {
var realReq = new Request(opts.method, opts.uri, opts.body, opts.qs);
realReq.callback = callback;
self.requests.push(realReq);
var req = new Request(opts, callback);
console.log("HTTP backend received request: %s", req);
self.requests.push(req);
var abort = function() {
var idx = self.requests.indexOf(req);
if (idx >= 0) {
console.log("Aborting HTTP request: %s %s", opts.method,
opts.uri);
self.requests.splice(idx, 1);
req.callback("aborted");
}
};
return {
abort: abort
};
};
}
HttpBackend.prototype = {
@@ -27,6 +41,7 @@ HttpBackend.prototype = {
var defer = q.defer();
var self = this;
var flushed = 0;
var triedWaiting = false;
console.log(
"HTTP backend flushing... (path=%s numToFlush=%s)", path, numToFlush
);
@@ -48,6 +63,12 @@ HttpBackend.prototype = {
setTimeout(tryFlush, 0);
}
}
else if (flushed === 0 && !triedWaiting) {
// we may not have made the request yet, wait a generous amount of
// time before giving up.
setTimeout(tryFlush, 5);
triedWaiting = true;
}
else {
console.log(" no more flushes. [%s]", path);
defer.resolve();
@@ -139,22 +160,32 @@ HttpBackend.prototype = {
* @return {Request} An expected request.
*/
when: function(method, path, data) {
var pendingReq = new Request(method, path, data);
var pendingReq = new ExpectedRequest(method, path, data);
this.expectedRequests.push(pendingReq);
return pendingReq;
}
};
function Request(method, path, data, queryParams) {
/**
* Represents the expectation of a request.
*
* <p>Includes the conditions to be matched against, the checks to be made,
* and the response to be returned.
*
* @constructor
* @param {string} method
* @param {string} path
* @param {object?} data
*/
function ExpectedRequest(method, path, data) {
this.method = method;
this.path = path;
this.data = data;
this.queryParams = queryParams;
this.callback = null;
this.response = null;
this.checks = [];
}
Request.prototype = {
ExpectedRequest.prototype = {
/**
* Execute a check when this request has been satisfied.
* @param {Function} fn The function to execute.
@@ -199,6 +230,44 @@ Request.prototype = {
}
};
/**
* Represents a request made by the app.
*
* @constructor
* @param {object} opts opts passed to request()
* @param {function} callback
*/
function Request(opts, callback) {
this.opts = opts;
this.callback = callback;
Object.defineProperty(this, 'method', {
get: function() { return opts.method; }
});
Object.defineProperty(this, 'path', {
get: function() { return opts.uri; }
});
Object.defineProperty(this, 'data', {
get: function() { return opts.body; }
});
Object.defineProperty(this, 'queryParams', {
get: function() { return opts.qs; }
});
Object.defineProperty(this, 'headers', {
get: function() { return opts.headers || {}; }
});
}
Request.prototype = {
toString: function() {
return this.method + " " + this.path;
},
};
/**
* The HttpBackend class.
*/
+57 -7
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,11 +61,11 @@ module.exports.mkEvent = function(opts) {
var event = {
type: opts.type,
room_id: opts.room,
user_id: opts.user,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
event_id: "$" + Math.random() + "-" + Math.random()
};
if (opts.skey) {
if (opts.skey !== undefined) {
event.state_key = opts.skey;
}
else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
@@ -88,8 +88,8 @@ module.exports.mkPresence = function(opts) {
var event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: {
user_id: opts.user,
avatar_url: opts.url,
displayname: opts.name,
last_active_ago: opts.ago,
@@ -104,8 +104,8 @@ module.exports.mkPresence = function(opts) {
* @param {Object} opts Values for the membership.
* @param {string} opts.room The room ID for the event.
* @param {string} opts.mship The content.membership for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.skey The other user ID for the event if applicable
* @param {string} opts.sender The sender user ID for the event.
* @param {string} opts.skey The target user ID for the event if applicable
* e.g. for invites/bans.
* @param {string} opts.name The content.displayname for the event.
* @param {string} opts.url The content.avatar_url for the event.
@@ -115,7 +115,7 @@ module.exports.mkPresence = function(opts) {
module.exports.mkMembership = function(opts) {
opts.type = "m.room.member";
if (!opts.skey) {
opts.skey = opts.user;
opts.skey = opts.sender || opts.user;
}
if (!opts.mship) {
throw new Error("Missing .mship => " + JSON.stringify(opts));
@@ -151,3 +151,53 @@ module.exports.mkMessage = function(opts) {
};
return module.exports.mkEvent(opts);
};
/**
* make the test fail, with the given exception
*
* <p>This is useful for use with integration tests which use asyncronous
* methods: it can be added as a 'catch' handler in a promise chain.
*
* @param {Error} err exception to be reported
*
* @deprecated
* It turns out there are easier ways of doing this. Just use nodeify():
*
* it("should not throw", function(done) {
* asynchronousMethod().then(function() {
* // some tests
* }).nodeify(done);
* });
*
* @example
* it("should not throw", function(done) {
* asynchronousMethod().then(function() {
* // some tests
* }).catch(utils.failTest).done(done);
* });
*/
module.exports.failTest = function(err) {
expect(true).toBe(false, "Testfunc threw: " + err.stack);
};
/**
* A mock implementation of webstorage
*
* @constructor
*/
module.exports.MockStorageApi = function() {
this.data = {};
};
module.exports.MockStorageApi.prototype = {
setItem: function(k, v) {
this.data[k] = v;
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
}
};
+92
View File
@@ -0,0 +1,92 @@
"use strict";
var ContentRepo = require("../../lib/content-repo");
var testUtils = require("../test-utils");
describe("ContentRepo", function() {
var baseUrl = "https://my.home.server";
beforeEach(function() {
testUtils.beforeEach(this);
});
describe("getHttpUriForMxc", function() {
it("should do nothing to HTTP URLs when allowing direct links", function() {
var httpUrl = "http://example.com/image.jpeg";
expect(
ContentRepo.getHttpUriForMxc(
baseUrl, httpUrl, undefined, undefined, undefined, true
)
).toEqual(httpUrl);
});
it("should return the empty string HTTP URLs by default", function() {
var httpUrl = "http://example.com/image.jpeg";
expect(ContentRepo.getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
});
it("should return a download URL if no width/height/resize are specified",
function() {
var mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v1/download/server.name/resourceid"
);
});
it("should return the empty string for null input", function() {
expect(ContentRepo.getHttpUriForMxc(null)).toEqual("");
});
it("should return a thumbnail URL if a width/height/resize is specified",
function() {
var mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
"?width=32&height=64&method=crop"
);
});
it("should put fragments from mxc:// URIs after any query parameters",
function() {
var mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
"?width=32#automade"
);
});
it("should put fragments from mxc:// URIs at the end of the HTTP URI",
function() {
var mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v1/download/server.name/resourceid#automade"
);
});
});
describe("getIdenticonUri", function() {
it("should do nothing for null input", function() {
expect(ContentRepo.getIdenticonUri(null)).toEqual(null);
});
it("should set w/h by default to 96", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar")).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foobar" +
"?width=96&height=96"
);
});
it("should be able to set custom w/h", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foobar" +
"?width=32&height=64"
);
});
it("should URL encode the identicon string", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foo%23bar" +
"?width=32&height=64"
);
});
});
});
+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([2, 0, 0]);
});
});
+370
View File
@@ -0,0 +1,370 @@
"use strict";
var sdk = require("../..");
var EventTimeline = sdk.EventTimeline;
var utils = require("../test-utils");
function mockRoomStates(timeline) {
timeline._startState = utils.mock(sdk.RoomState, "startState");
timeline._endState = utils.mock(sdk.RoomState, "endState");
}
describe("EventTimeline", function() {
var roomId = "!foo:bar";
var userA = "@alice:bar";
var userB = "@bertha:bar";
var timeline;
beforeEach(function() {
utils.beforeEach(this);
// XXX: this is a horrid hack; should use sinon or something instead to mock
var timelineSet = { room: { roomId: roomId }};
timelineSet.room.getUnfilteredTimelineSet = function() { return timelineSet; };
timeline = new EventTimeline(timelineSet);
});
describe("construction", function() {
it("getRoomId should get room id", function() {
var v = timeline.getRoomId();
expect(v).toEqual(roomId);
});
});
describe("initialiseState", function() {
beforeEach(function() {
mockRoomStates(timeline);
});
it("should copy state events to start and end state", function() {
var events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA,
event: true,
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB,
event: true,
content: { name: "New room" },
})
];
timeline.initialiseState(events);
expect(timeline._startState.setStateEvents).toHaveBeenCalledWith(
events
);
expect(timeline._endState.setStateEvents).toHaveBeenCalledWith(
events
);
});
it("should raise an exception if called after events are added", function() {
var event =
utils.mkMessage({
room: roomId, user: userA, msg: "Adam stole the plushies",
event: true,
});
var state = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA,
event: true,
})
];
expect(function() { timeline.initialiseState(state); }).not.toThrow();
timeline.addEvent(event, false);
expect(function() { timeline.initialiseState(state); }).toThrow();
});
});
describe("paginationTokens", function() {
it("pagination tokens should start null", function() {
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toBe(null);
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toBe(null);
});
it("setPaginationToken should set token", function() {
timeline.setPaginationToken("back", EventTimeline.BACKWARDS);
timeline.setPaginationToken("fwd", EventTimeline.FORWARDS);
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back");
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toEqual("fwd");
});
});
describe("neighbouringTimelines", function() {
it("neighbouring timelines should start null", function() {
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(null);
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(null);
});
it("setNeighbouringTimeline should set neighbour", function() {
var prev = {a: "a"};
var next = {b: "b"};
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(prev);
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(next);
});
it("setNeighbouringTimeline should throw if called twice", function() {
var prev = {a: "a"};
var next = {b: "b"};
expect(function() {
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
}).not.toThrow();
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS))
.toBe(prev);
expect(function() {
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
}).toThrow();
expect(function() {
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
}).not.toThrow();
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS))
.toBe(next);
expect(function() {
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
}).toThrow();
});
});
describe("addEvent", function() {
beforeEach(function() {
mockRoomStates(timeline);
});
var events = [
utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry",
event: true,
}),
utils.mkMessage({
room: roomId, user: userB, msg: "nom nom nom",
event: true,
}),
];
it("should be able to add events to the end", function() {
timeline.addEvent(events[0], false);
var initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], false);
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getEvents()[0]).toEqual(events[0]);
expect(timeline.getEvents()[1]).toEqual(events[1]);
});
it("should be able to add events to the start", function() {
timeline.addEvent(events[0], true);
var initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], true);
expect(timeline.getBaseIndex()).toEqual(initialIndex + 1);
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getEvents()[0]).toEqual(events[1]);
expect(timeline.getEvents()[1]).toEqual(events[0]);
});
it("should set event.sender for new and old events", function() {
var sentinel = {
userId: userA,
membership: "join",
name: "Alice"
};
var oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
};
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
.andCallFake(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" }
});
var oldEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "Old Room Name" }
});
timeline.addEvent(newEv, false);
expect(newEv.sender).toEqual(sentinel);
timeline.addEvent(oldEv, true);
expect(oldEv.sender).toEqual(oldSentinel);
});
it("should set event.target for new and old m.room.member events",
function() {
var sentinel = {
userId: userA,
membership: "join",
name: "Alice"
};
var oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
};
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
.andCallFake(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
});
var oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true
});
timeline.addEvent(newEv, false);
expect(newEv.target).toEqual(sentinel);
timeline.addEvent(oldEv, true);
expect(oldEv.target).toEqual(oldSentinel);
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for new events", function() {
var events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room"
}
})
];
timeline.addEvent(events[0], false);
timeline.addEvent(events[1], false);
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
toHaveBeenCalledWith([events[0]]);
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
toHaveBeenCalledWith([events[1]]);
expect(events[0].forwardLooking).toBe(true);
expect(events[1].forwardLooking).toBe(true);
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
not.toHaveBeenCalled();
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for old events", function() {
var events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room"
}
})
];
timeline.addEvent(events[0], true);
timeline.addEvent(events[1], true);
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
toHaveBeenCalledWith([events[0]]);
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
toHaveBeenCalledWith([events[1]]);
expect(events[0].forwardLooking).toBe(false);
expect(events[1].forwardLooking).toBe(false);
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
not.toHaveBeenCalled();
});
});
describe("removeEvent", function() {
var events = [
utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry",
event: true,
}),
utils.mkMessage({
room: roomId, user: userB, msg: "nom nom nom",
event: true,
}),
utils.mkMessage({
room: roomId, user: userB, msg: "piiie",
event: true,
}),
];
it("should remove events", function() {
timeline.addEvent(events[0], false);
timeline.addEvent(events[1], false);
expect(timeline.getEvents().length).toEqual(2);
var ev = timeline.removeEvent(events[0].getId());
expect(ev).toBe(events[0]);
expect(timeline.getEvents().length).toEqual(1);
ev = timeline.removeEvent(events[1].getId());
expect(ev).toBe(events[1]);
expect(timeline.getEvents().length).toEqual(0);
});
it("should update baseIndex", function() {
timeline.addEvent(events[0], false);
timeline.addEvent(events[1], true);
timeline.addEvent(events[2], false);
expect(timeline.getEvents().length).toEqual(3);
expect(timeline.getBaseIndex()).toEqual(1);
timeline.removeEvent(events[2].getId());
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getBaseIndex()).toEqual(1);
timeline.removeEvent(events[1].getId());
expect(timeline.getEvents().length).toEqual(1);
expect(timeline.getBaseIndex()).toEqual(0);
});
// this is basically https://github.com/vector-im/vector-web/issues/937
// - removing the last event got baseIndex into such a state that
// further addEvent(ev, false) calls made the index increase.
it("should not make baseIndex assplode when removing the last event",
function() {
timeline.addEvent(events[0], true);
timeline.removeEvent(events[0].getId());
var initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], false);
timeline.addEvent(events[2], false);
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
});
});
});
+50
View File
@@ -0,0 +1,50 @@
"use strict";
var sdk = require("../..");
var Filter = sdk.Filter;
var utils = require("../test-utils");
describe("Filter", function() {
var filterId = "f1lt3ring15g00d4ursoul";
var userId = "@sir_arthur_david:humming.tiger";
var filter;
beforeEach(function() {
utils.beforeEach(this);
filter = new Filter(userId);
});
describe("fromJson", function() {
it("create a new Filter from the provided values", function() {
var definition = {
event_fields: ["type", "content"]
};
var f = Filter.fromJson(userId, filterId, definition);
expect(f.getDefinition()).toEqual(definition);
expect(f.userId).toEqual(userId);
expect(f.filterId).toEqual(filterId);
});
});
describe("setTimelineLimit", function() {
it("should set room.timeline.limit of the filter definition", function() {
filter.setTimelineLimit(10);
expect(filter.getDefinition()).toEqual({
room: {
timeline: {
limit: 10
}
}
});
});
});
describe("setDefinition/getDefinition", function() {
it("should set and get the filter body", function() {
var definition = {
event_format: "client"
};
filter.setDefinition(definition);
expect(filter.getDefinition()).toEqual(definition);
});
});
});
+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";
var q = require("q");
var sdk = require("../..");
var utils = require("../test-utils");
var InteractiveAuth = sdk.InteractiveAuth;
var MatrixError = sdk.MatrixError;
describe("InteractiveAuth", function() {
beforeEach(function() {
utils.beforeEach(this);
});
it("should start an auth stage and complete it", function(done) {
var doRequest = jasmine.createSpy('doRequest');
var startAuthStage = jasmine.createSpy('startAuthStage');
var ia = new InteractiveAuth({
doRequest: doRequest,
startAuthStage: startAuthStage,
authData: {
session: "sessionId",
flows: [
{ stages: ["logintype"] },
],
params: {
"logintype": { param: "aa" },
},
},
});
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams("logintype")).toEqual({
param: "aa",
});
// first we expect a call here
startAuthStage.andCallFake(function(stage) {
expect(stage).toEqual("logintype");
ia.submitAuthDict({
type: "logintype",
foo: "bar",
});
});
// .. which should trigger a call here
var requestRes = {"a": "b"};
doRequest.andCallFake(function(authData) {
expect(authData).toEqual({
session: "sessionId",
type: "logintype",
foo: "bar",
});
return q(requestRes);
});
ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest.calls.length).toEqual(1);
expect(startAuthStage.calls.length).toEqual(1);
}).catch(utils.failTest).done(done);
});
it("should make a request if no authdata is provided", function(done) {
var doRequest = jasmine.createSpy('doRequest');
var startAuthStage = jasmine.createSpy('startAuthStage');
var ia = new InteractiveAuth({
doRequest: doRequest,
startAuthStage: startAuthStage,
});
expect(ia.getSessionId()).toBe(undefined);
expect(ia.getStageParams("logintype")).toBe(undefined);
// first we expect a call to doRequest
doRequest.andCallFake(function(authData) {
console.log("request1", authData);
expect(authData).toBe(null);
var err = new MatrixError({
session: "sessionId",
flows: [
{ stages: ["logintype"] },
],
params: {
"logintype": { param: "aa" },
},
});
err.httpStatus = 401;
throw err;
});
// .. which should be followed by a call to startAuthStage
var requestRes = {"a": "b"};
startAuthStage.andCallFake(function(stage) {
expect(stage).toEqual("logintype");
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams("logintype")).toEqual({
param: "aa",
});
// submitAuthDict should trigger another call to doRequest
doRequest.andCallFake(function(authData) {
console.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
type: "logintype",
foo: "bar",
});
return q(requestRes);
});
ia.submitAuthDict({
type: "logintype",
foo: "bar",
});
});
ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest.calls.length).toEqual(2);
expect(startAuthStage.calls.length).toEqual(1);
}).catch(utils.failTest).done(done);
});
});
+516
View File
@@ -0,0 +1,516 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var MatrixClient = sdk.MatrixClient;
var utils = require("../test-utils");
describe("MatrixClient", function() {
var userId = "@alice:bar";
var identityServerUrl = "https://identity.server";
var identityServerDomain = "identity.server";
var client, store, scheduler;
var KEEP_ALIVE_PATH = "/_matrix/client/versions";
var PUSH_RULES_RESPONSE = {
method: "GET",
path: "/pushrules/",
data: {}
};
var FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter";
var FILTER_RESPONSE = {
method: "POST",
path: FILTER_PATH,
data: { filter_id: "f1lt3r" }
};
var SYNC_DATA = {
next_batch: "s_5_3",
presence: { events: [] },
rooms: {}
};
var SYNC_RESPONSE = {
method: "GET",
path: "/sync",
data: SYNC_DATA
};
var httpLookups = [
// items are objects which look like:
// {
// method: "GET",
// path: "/initialSync",
// data: {},
// error: { errcode: M_FORBIDDEN } // if present will reject promise,
// expectBody: {} // additional expects on the body
// expectQueryParams: {} // additional expects on query params
// thenCall: function(){} // function to call *AFTER* returning response.
// }
// items are popped off when processed and block if no items left.
];
var accept_keepalives;
var pendingLookup = null;
function httpReq(cb, method, path, qp, data, prefix) {
if (path === KEEP_ALIVE_PATH && accept_keepalives) {
return q();
}
var next = httpLookups.shift();
var logLine = (
"MatrixClient[UT] RECV " + method + " " + path + " " +
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
);
console.log(logLine);
if (!next) { // no more things to return
if (pendingLookup) {
if (pendingLookup.method === method && pendingLookup.path === path) {
return pendingLookup.promise;
}
// >1 pending thing, and they are different, whine.
expect(false).toBe(
true, ">1 pending request. You should probably handle them. " +
"PENDING: " + JSON.stringify(pendingLookup) + " JUST GOT: " +
method + " " + path
);
}
pendingLookup = {
promise: q.defer().promise,
method: method,
path: path
};
return pendingLookup.promise;
}
if (next.path === path && next.method === method) {
console.log(
"MatrixClient[UT] Matched. Returning " +
(next.error ? "BAD" : "GOOD") + " response"
);
if (next.expectBody) {
expect(next.expectBody).toEqual(data);
}
if (next.expectQueryParams) {
Object.keys(next.expectQueryParams).forEach(function(k) {
expect(qp[k]).toEqual(next.expectQueryParams[k]);
});
}
if (next.thenCall) {
process.nextTick(next.thenCall, 0); // next tick so we return first.
}
if (next.error) {
return q.reject({
errcode: next.error.errcode,
httpStatus: next.error.httpStatus,
name: next.error.errcode,
message: "Expected testing error",
data: next.error
});
}
return q(next.data);
}
expect(true).toBe(false, "Expected different request. " + logLine);
return q.defer().promise;
}
beforeEach(function() {
utils.beforeEach(this);
jasmine.Clock.useMock();
scheduler = jasmine.createSpyObj("scheduler", [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction"
]);
store = jasmine.createSpyObj("store", [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"setSyncToken", "storeEvents", "storeRoom", "storeUser",
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter"
]);
client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl,
accessToken: "my.access.token",
request: function() {}, // NOP
store: store,
scheduler: scheduler,
userId: userId
});
// FIXME: We shouldn't be yanking _http like this.
client._http = jasmine.createSpyObj("httpApi", [
"authedRequest", "authedRequestWithPrefix", "getContentUri",
"request", "requestWithPrefix", "uploadContent"
]);
client._http.authedRequest.andCallFake(httpReq);
client._http.authedRequestWithPrefix.andCallFake(httpReq);
client._http.requestWithPrefix.andCallFake(httpReq);
client._http.request.andCallFake(httpReq);
// set reasonable working defaults
accept_keepalives = true;
pendingLookup = null;
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
});
afterEach(function() {
// need to re-stub the requests with NOPs because there are no guarantees
// clients from previous tests will be GC'd before the next test. This
// means they may call /events and then fail an expect() which will fail
// a DIFFERENT test (pollution between tests!) - we return unresolved
// promises to stop the client from continuing to run.
client._http.authedRequest.andCallFake(function() {
return q.defer().promise;
});
client._http.authedRequestWithPrefix.andCallFake(function() {
return q.defer().promise;
});
});
it("should not POST /filter if a matching filter already exists", function(done) {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
var filterId = "ehfewf";
store.getFilterIdByName.andReturn(filterId);
var filter = new sdk.Filter(0, filterId);
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
store.getFilter.andReturn(filter);
client.startClient();
client.on("sync", function syncListener(state) {
if (state === "SYNCING") {
expect(httpLookups.length).toEqual(0);
client.removeListener("sync", syncListener);
done();
}
});
});
describe("getSyncState", function() {
it("should return null if the client isn't started", function() {
expect(client.getSyncState()).toBeNull();
});
it("should return the same sync state as emitted sync events", function(done) {
client.on("sync", function syncListener(state) {
expect(state).toEqual(client.getSyncState());
if (state === "SYNCING") {
client.removeListener("sync", syncListener);
done();
}
});
client.startClient();
});
});
describe("getOrCreateFilter", function() {
it("should POST createFilter if no id is present in localStorage", function() {
});
it("should use an existing filter if id is present in localStorage", function() {
});
it("should handle localStorage filterId missing from the server", function(done) {
function getFilterName(userId, suffix) {
// scope this on the user ID because people may login on many accounts
// and they all need to be stored!
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
}
var invalidFilterId = 'invalidF1lt3r';
httpLookups = [];
httpLookups.push({
method: "GET",
path: FILTER_PATH + '/' + invalidFilterId,
error: {
errcode: "M_UNKNOWN",
name: "M_UNKNOWN",
message: "No row found",
data: { errcode: "M_UNKNOWN", error: "No row found" },
httpStatus: 404
}
});
httpLookups.push(FILTER_RESPONSE);
store.getFilterIdByName.andReturn(invalidFilterId);
var filterName = getFilterName(client.credentials.userId);
client.store.setFilterIdByName(filterName, invalidFilterId);
var filter = new sdk.Filter(client.credentials.userId);
client.getOrCreateFilter(filterName, filter).then(function(filterId) {
expect(filterId).toEqual(FILTER_RESPONSE.data.filter_id);
done();
});
});
});
describe("retryImmediately", function() {
it("should return false if there is no request waiting", function() {
client.startClient();
expect(client.retryImmediately()).toBe(false);
});
it("should work on /filter", function(done) {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }
});
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
client.on("sync", function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(2);
expect(client.retryImmediately()).toBe(true);
jasmine.Clock.tick(1);
} else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
} else {
// unexpected state transition!
expect(state).toEqual(null);
}
});
client.startClient();
});
it("should work on /sync", function(done) {
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA
});
client.on("sync", function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(1);
expect(client.retryImmediately()).toBe(
true, "retryImmediately returned false"
);
jasmine.Clock.tick(1);
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
jasmine.Clock.tick(10000);
} else if (state === "SYNCING" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
}
});
client.startClient();
});
it("should work on /pushrules", function(done) {
httpLookups = [];
httpLookups.push({
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" }
});
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
client.on("sync", function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(3);
expect(client.retryImmediately()).toBe(true);
jasmine.Clock.tick(1);
} else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
} else {
// unexpected state transition!
expect(state).toEqual(null);
}
});
client.startClient();
});
});
describe("emitted sync events", function() {
function syncChecker(expectedStates, done) {
return function syncListener(state, old) {
var expected = expectedStates.shift();
console.log(
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected
);
if (!expected) {
done();
return;
}
expect(state).toEqual(expected[0]);
expect(old).toEqual(expected[1]);
if (expectedStates.length === 0) {
client.removeListener("sync", syncListener);
done();
}
// standard retry time is 5 to 10 seconds
jasmine.Clock.tick(10000);
};
}
it("should transition null -> PREPARED after the first /sync", function(done) {
var expectedStates = [];
expectedStates.push(["PREPARED", null]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition null -> ERROR after a failed /filter", function(done) {
var expectedStates = [];
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }
});
expectedStates.push(["ERROR", null]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition ERROR -> PREPARED after /sync if prev failed",
function(done) {
var expectedStates = [];
accept_keepalives = false;
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, data: {}
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA
});
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["PREPARED", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition PREPARED -> SYNCING after /sync", function(done) {
var expectedStates = [];
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition SYNCING -> ERROR after a failed /sync", function(done) {
accept_keepalives = false;
var expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
xit("should transition ERROR -> SYNCING after /sync if prev failed",
function(done) {
var expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
});
httpLookups.push(SYNC_RESPONSE);
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition SYNCING -> SYNCING on subsequent /sync successes",
function(done) {
var expectedStates = [];
httpLookups.push(SYNC_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["SYNCING", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
accept_keepalives = false;
var expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["ERROR", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
});
describe("inviteByEmail", function() {
var roomId = "!foo:bar";
it("should send an invite HTTP POST", function() {
httpLookups = [{
method: "POST",
path: "/rooms/!foo%3Abar/invite",
data: {},
expectBody: {
id_server: identityServerDomain,
medium: "email",
address: "alice@gmail.com"
}
}];
client.inviteByEmail(roomId, "alice@gmail.com");
expect(httpLookups.length).toEqual(0);
});
});
describe("guest rooms", function() {
it("should only do /sync calls (without filter/pushrules)", function(done) {
httpLookups = []; // no /pushrules or /filter
httpLookups.push({
method: "GET",
path: "/sync",
data: SYNC_DATA,
thenCall: function() {
done();
}
});
client.setGuest(true);
client.startClient();
});
xit("should be able to peek into a room using peekInRoom", function(done) {
});
});
});
+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();
});
});
});
+68
View File
@@ -15,6 +15,40 @@ describe("RoomMember", function() {
member = new RoomMember(roomId, userA);
});
describe("getAvatarUrl", function() {
var hsUrl = "https://my.home.server";
it("should return the URL from m.room.member preferentially", function() {
member.events.member = utils.mkEvent({
event: true,
type: "m.room.member",
skey: userA,
room: roomId,
user: userA,
content: {
membership: "join",
avatar_url: "mxc://flibble/wibble"
}
});
var url = member.getAvatarUrl(hsUrl);
// we don't care about how the mxc->http conversion is done, other
// than it contains the mxc body.
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
});
it("should return an identicon HTTP URL if allowDefault was set and there " +
"was no m.room.member event", function() {
var url = member.getAvatarUrl(hsUrl, 64, 64, "crop", true);
expect(url.indexOf("http")).toEqual(0); // don't care about form
});
it("should return nothing if there is no m.room.member and allowDefault=false",
function() {
var url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
expect(url).toEqual(null);
});
});
describe("setPowerLevelEvent", function() {
it("should set 'powerLevel' and 'powerLevelNorm'.", function() {
var event = utils.mkEvent({
@@ -68,6 +102,37 @@ describe("RoomMember", function() {
member.setPowerLevelEvent(event); // no-op
expect(emitCount).toEqual(1);
});
it("should honour power levels of zero.",
function() {
var event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@alice:bar": 0,
}
},
event: true
});
var emitCount = 0;
// set the power level to something other than zero or we
// won't get an event
member.powerLevel = 1;
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember.userId).toEqual('@alice:bar');
expect(emitMember.powerLevel).toEqual(0);
expect(emitEvent).toEqual(event);
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(0);
expect(emitCount).toEqual(1);
});
});
describe("setTypingEvent", function() {
@@ -167,6 +232,9 @@ describe("RoomMember", function() {
}),
joinEvent
];
},
getUserIdsWithDisplayName: function(displayName) {
return [userA, userC];
}
};
expect(member.name).toEqual(userA); // default = user_id
+153
View File
@@ -279,4 +279,157 @@ describe("RoomState", function() {
);
});
});
describe("maySendStateEvent", function() {
it("should say non-joined members may not send state",
function() {
expect(state.maySendStateEvent(
'm.room.name', "@nobody:nowhere"
)).toEqual(false);
});
it("should say any member may send state with no power level event",
function() {
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
});
it("should say members with power >=50 may send state with power level event " +
"but no state default",
function() {
var powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
// state_default: 50, "intentionally left blank"
events_default: 25,
users: {
}
}
};
powerLevelEvent.content.users[userA] = 50;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
});
it("should obey state_default",
function() {
var powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 30,
events_default: 25,
users: {
}
}
};
powerLevelEvent.content.users[userA] = 30;
powerLevelEvent.content.users[userB] = 29;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
});
it("should honour explicit event power levels in the power_levels event",
function() {
var powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
events: {
"m.room.other_thing": 76
},
users_default: 10,
state_default: 50,
events_default: 25,
users: {
}
}
};
powerLevelEvent.content.users[userA] = 80;
powerLevelEvent.content.users[userB] = 50;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(true);
expect(state.maySendStateEvent('m.room.other_thing', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.other_thing', userB)).toEqual(false);
});
});
describe("maySendEvent", function() {
it("should say non-joined members may not send events",
function() {
expect(state.maySendEvent(
'm.room.message', "@nobody:nowhere"
)).toEqual(false);
expect(state.maySendMessage("@nobody:nowhere")).toEqual(false);
});
it("should say any member may send events with no power level event",
function() {
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendMessage(userA)).toEqual(true);
});
it("should obey events_default",
function() {
var powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 30,
events_default: 25,
users: {
}
}
};
powerLevelEvent.content.users[userA] = 26;
powerLevelEvent.content.users[userB] = 24;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendEvent('m.room.message', userB)).toEqual(false);
expect(state.maySendMessage(userA)).toEqual(true);
expect(state.maySendMessage(userB)).toEqual(false);
});
it("should honour explicit event power levels in the power_levels event",
function() {
var powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
events: {
"m.room.other_thing": 33
},
users_default: 10,
state_default: 50,
events_default: 25,
users: {
}
}
};
powerLevelEvent.content.users[userA] = 40;
powerLevelEvent.content.users[userB] = 30;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendEvent('m.room.message', userB)).toEqual(true);
expect(state.maySendMessage(userA)).toEqual(true);
expect(state.maySendMessage(userB)).toEqual(true);
expect(state.maySendEvent('m.room.other_thing', userA)).toEqual(true);
expect(state.maySendEvent('m.room.other_thing', userB)).toEqual(false);
});
});
});
+931 -213
View File
File diff suppressed because it is too large Load Diff
+464
View File
@@ -0,0 +1,464 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var EventTimeline = sdk.EventTimeline;
var TimelineWindow = sdk.TimelineWindow;
var TimelineIndex = require("../../lib/timeline-window").TimelineIndex;
var utils = require("../test-utils");
var ROOM_ID = "roomId";
var USER_ID = "userId";
/*
* create a timeline with a bunch (default 3) events.
* baseIndex is 1 by default.
*/
function createTimeline(numEvents, baseIndex) {
if (numEvents === undefined) { numEvents = 3; }
if (baseIndex === undefined) { baseIndex = 1; }
// XXX: this is a horrid hack
var timelineSet = { room: { roomId: ROOM_ID }};
timelineSet.room.getUnfilteredTimelineSet = function() { return timelineSet; };
var timeline = new EventTimeline(timelineSet);
// add the events after the baseIndex first
addEventsToTimeline(timeline, numEvents - baseIndex, false);
// then add those before the baseIndex
addEventsToTimeline(timeline, baseIndex, true);
expect(timeline.getBaseIndex()).toEqual(baseIndex);
return timeline;
}
function addEventsToTimeline(timeline, numEvents, atStart) {
for (var i = 0; i < numEvents; i++) {
timeline.addEvent(
utils.mkMessage({
room: ROOM_ID, user: USER_ID,
event: true,
}), atStart
);
}
}
/*
* create a pair of linked timelines
*/
function createLinkedTimelines() {
var tl1 = createTimeline();
var tl2 = createTimeline();
tl1.setNeighbouringTimeline(tl2, EventTimeline.FORWARDS);
tl2.setNeighbouringTimeline(tl1, EventTimeline.BACKWARDS);
return [tl1, tl2];
}
describe("TimelineIndex", function() {
beforeEach(function() {
utils.beforeEach(this);
});
describe("minIndex", function() {
it("should return the min index relative to BaseIndex", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
expect(timelineIndex.minIndex()).toEqual(-1);
});
});
describe("maxIndex", function() {
it("should return the max index relative to BaseIndex", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
expect(timelineIndex.maxIndex()).toEqual(2);
});
});
describe("advance", function() {
it("should advance up to the end of the timeline", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
var result = timelineIndex.advance(3);
expect(result).toEqual(2);
expect(timelineIndex.index).toEqual(2);
});
it("should retreat back to the start of the timeline", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
var result = timelineIndex.advance(-2);
expect(result).toEqual(-1);
expect(timelineIndex.index).toEqual(-1);
});
it("should advance into the next timeline", function() {
var timelines = createLinkedTimelines();
var tl1 = timelines[0], tl2 = timelines[1];
// initialise the index pointing at the end of the first timeline
var timelineIndex = new TimelineIndex(tl1, 2);
var result = timelineIndex.advance(1);
expect(result).toEqual(1);
expect(timelineIndex.timeline).toBe(tl2);
// we expect the index to be the zero (ie, the same as the
// BaseIndex), because the BaseIndex points at the second event,
// and we've advanced past the first.
expect(timelineIndex.index).toEqual(0);
});
it("should retreat into the previous timeline", function() {
var timelines = createLinkedTimelines();
var tl1 = timelines[0], tl2 = timelines[1];
// initialise the index pointing at the start of the second
// timeline
var timelineIndex = new TimelineIndex(tl2, -1);
var result = timelineIndex.advance(-1);
expect(result).toEqual(-1);
expect(timelineIndex.timeline).toBe(tl1);
expect(timelineIndex.index).toEqual(1);
});
});
describe("retreat", function() {
it("should retreat up to the start of the timeline", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
var result = timelineIndex.retreat(2);
expect(result).toEqual(1);
expect(timelineIndex.index).toEqual(-1);
});
});
});
describe("TimelineWindow", function() {
/**
* create a dummy eventTimelineSet and client, and a TimelineWindow
* attached to them.
*/
var timelineSet, client;
function createWindow(timeline, opts) {
timelineSet = {};
client = {};
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineSet0).toBe(timelineSet);
return q(timeline);
};
return new TimelineWindow(client, timelineSet, opts);
}
beforeEach(function() {
utils.beforeEach(this);
});
describe("load", function() {
it("should initialise from the live timeline", function(done) {
var liveTimeline = createTimeline();
var room = {};
room.getLiveTimeline = function() { return liveTimeline; };
var timelineWindow = new TimelineWindow(undefined, room);
timelineWindow.load(undefined, 2).then(function() {
var expectedEvents = liveTimeline.getEvents().slice(1);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
});
it("should initialise from a specific event", function(done) {
var timeline = createTimeline();
var eventId = timeline.getEvents()[1].getId();
var timelineSet = {};
var client = {};
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineSet0).toBe(timelineSet);
expect(eventId0).toEqual(eventId);
return q(timeline);
};
var timelineWindow = new TimelineWindow(client, timelineSet);
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
});
it("canPaginate should return false until load has returned",
function(done) {
var timeline = createTimeline();
timeline.setPaginationToken("toktok1", EventTimeline.BACKWARDS);
timeline.setPaginationToken("toktok2", EventTimeline.FORWARDS);
var eventId = timeline.getEvents()[1].getId();
var timelineSet = {};
var client = {};
var timelineWindow = new TimelineWindow(client, timelineSet);
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return q(timeline);
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
}).catch(utils.failTest).done(done);
});
});
describe("pagination", function() {
it("should be able to advance across the initial timeline",
function(done) {
var timeline = createTimeline();
var eventId = timeline.getEvents()[1].getId();
var timelineWindow = createWindow(timeline);
timelineWindow.load(eventId, 1).then(function() {
var expectedEvents = [timeline.getEvents()[1]];
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = timeline.getEvents().slice(1);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
}).catch(utils.failTest).done(done);
});
it("should advance into next timeline", function(done) {
var tls = createLinkedTimelines();
var eventId = tls[0].getEvents()[1].getId();
var timelineWindow = createWindow(tls[0], {windowLimit: 5});
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = tls[0].getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = tls[0].getEvents()
.concat(tls[1].getEvents().slice(0, 2));
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
// the windowLimit should have made us drop an event from
// tls[0]
var expectedEvents = tls[0].getEvents().slice(1)
.concat(tls[1].getEvents());
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
}).catch(utils.failTest).done(done);
});
it("should retreat into previous timeline", function(done) {
var tls = createLinkedTimelines();
var eventId = tls[1].getEvents()[1].getId();
var timelineWindow = createWindow(tls[1], {windowLimit: 5});
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = tls[1].getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = tls[0].getEvents().slice(1, 3)
.concat(tls[1].getEvents());
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
// the windowLimit should have made us drop an event from
// tls[1]
var expectedEvents = tls[0].getEvents()
.concat(tls[1].getEvents().slice(0, 2));
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
}).catch(utils.failTest).done(done);
});
it("should make forward pagination requests", function(done) {
var timeline = createTimeline();
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
var timelineWindow = createWindow(timeline, {windowLimit: 5});
var eventId = timeline.getEvents()[1].getId();
client.paginateEventTimeline = function(timeline0, opts) {
expect(timeline0).toBe(timeline);
expect(opts.backwards).toBe(false);
expect(opts.limit).toEqual(2);
addEventsToTimeline(timeline, 3, false);
return q(true);
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = timeline.getEvents().slice(0, 5);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
});
it("should make backward pagination requests", function(done) {
var timeline = createTimeline();
timeline.setPaginationToken("toktok", EventTimeline.BACKWARDS);
var timelineWindow = createWindow(timeline, {windowLimit: 5});
var eventId = timeline.getEvents()[1].getId();
client.paginateEventTimeline = function(timeline0, opts) {
expect(timeline0).toBe(timeline);
expect(opts.backwards).toBe(true);
expect(opts.limit).toEqual(2);
addEventsToTimeline(timeline, 3, true);
return q(true);
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = timeline.getEvents().slice(1, 6);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
});
it("should limit the number of unsuccessful pagination requests",
function(done) {
var timeline = createTimeline();
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
var timelineWindow = createWindow(timeline, {windowLimit: 5});
var eventId = timeline.getEvents()[1].getId();
var paginateCount = 0;
client.paginateEventTimeline = function(timeline0, opts) {
expect(timeline0).toBe(timeline);
expect(opts.backwards).toBe(false);
expect(opts.limit).toEqual(2);
paginateCount += 1;
return q(true);
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2, true, 3);
}).then(function(success) {
expect(success).toBe(false);
expect(paginateCount).toEqual(3);
var expectedEvents = timeline.getEvents().slice(0, 3);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
}).catch(utils.failTest).done(done);
});
});
});
+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;