Compare commits

..

475 Commits

Author SHA1 Message Date
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
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
82 changed files with 10611 additions and 124589 deletions
+1 -3
View File
@@ -1,5 +1,3 @@
# Keep this file in sync with .npmignore.
.jsdoc
node_modules
.lock-wscript
@@ -8,7 +6,7 @@ coverage
lib-cov
out
reports
dist/browser-matrix-dev.js
/dist
# version file and tarball created by 'npm pack'
/git-revision.txt
-15
View File
@@ -1,15 +0,0 @@
# Keep this file in sync with .gitignore.
.jsdoc
node_modules
.lock-wscript
build/Release
coverage
lib-cov
out
reports
dist/browser-matrix-dev.js
# tarball created by 'npm pack'.
/matrix-js-sdk-*.tgz
+3
View File
@@ -0,0 +1,3 @@
language: node_js
node_js:
- node # Latest stable version of nodejs.
+338
View File
@@ -1,3 +1,341 @@
Changes in [0.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.0) (2016-11-18)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4...v0.7.0)
* Avoid a packetstorm of device queries on startup
[\#297](https://github.com/matrix-org/matrix-js-sdk/pull/297)
* E2E: Check devices to share keys with on each send
[\#295](https://github.com/matrix-org/matrix-js-sdk/pull/295)
* Apply unknown-keyshare mitigations
[\#296](https://github.com/matrix-org/matrix-js-sdk/pull/296)
* distinguish unknown users from deviceless users
[\#294](https://github.com/matrix-org/matrix-js-sdk/pull/294)
* Allow starting client with initialSyncLimit = 0
[\#293](https://github.com/matrix-org/matrix-js-sdk/pull/293)
* Make timeline-window _unpaginate public and rename to unpaginate
[\#289](https://github.com/matrix-org/matrix-js-sdk/pull/289)
* Send a STOPPED sync updated after call to stopClient
[\#286](https://github.com/matrix-org/matrix-js-sdk/pull/286)
* Fix bug in verifying megolm event senders
[\#292](https://github.com/matrix-org/matrix-js-sdk/pull/292)
* Handle decryption of events after they arrive
[\#288](https://github.com/matrix-org/matrix-js-sdk/pull/288)
* Fix examples.
[\#287](https://github.com/matrix-org/matrix-js-sdk/pull/287)
* Add a travis.yml
[\#278](https://github.com/matrix-org/matrix-js-sdk/pull/278)
* Encrypt all events, including 'm.call.*'
[\#277](https://github.com/matrix-org/matrix-js-sdk/pull/277)
* Ignore reshares of known megolm sessions
[\#276](https://github.com/matrix-org/matrix-js-sdk/pull/276)
* Log to the console on unknown session
[\#274](https://github.com/matrix-org/matrix-js-sdk/pull/274)
* Make it easier for SDK users to wrap prevailing the 'request' function
[\#273](https://github.com/matrix-org/matrix-js-sdk/pull/273)
Changes in [0.6.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4) (2016-11-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4-rc.2...v0.6.4)
* Change release script to pass version by environment variable
Changes in [0.6.4-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4-rc.2) (2016-11-02)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4-rc.1...v0.6.4-rc.2)
* Add getRoomTags method to client
[\#236](https://github.com/matrix-org/matrix-js-sdk/pull/236)
Changes in [0.6.4-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4-rc.1) (2016-11-02)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.3...v0.6.4-rc.1)
Breaking Changes
----------------
* Bundled version of the JS SDK are no longer versioned along with
source files in the dist/ directory. As of this release, they
will be included in the release tarball, but not the source
repository.
Other Changes
-------------
* More fixes to the release script
[\#272](https://github.com/matrix-org/matrix-js-sdk/pull/272)
* Update the release process to use github releases
[\#271](https://github.com/matrix-org/matrix-js-sdk/pull/271)
* Don't package the world when we release
[\#270](https://github.com/matrix-org/matrix-js-sdk/pull/270)
* Add ability to set a filter prior to the first /sync
[\#269](https://github.com/matrix-org/matrix-js-sdk/pull/269)
* Sign one-time keys, and verify their signatures
[\#243](https://github.com/matrix-org/matrix-js-sdk/pull/243)
* Check for duplicate message indexes for group messages
[\#241](https://github.com/matrix-org/matrix-js-sdk/pull/241)
* Rotate megolm sessions
[\#240](https://github.com/matrix-org/matrix-js-sdk/pull/240)
* Check recipient and sender in Olm messages
[\#239](https://github.com/matrix-org/matrix-js-sdk/pull/239)
* Consistency checks for E2E device downloads
[\#237](https://github.com/matrix-org/matrix-js-sdk/pull/237)
* Support User-Interactive auth for delete device
[\#235](https://github.com/matrix-org/matrix-js-sdk/pull/235)
* Utility to help with interactive auth
[\#234](https://github.com/matrix-org/matrix-js-sdk/pull/234)
* Fix sync breaking when an invalid filterId is in localStorage
[\#228](https://github.com/matrix-org/matrix-js-sdk/pull/228)
Changes in [0.6.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.3) (2016-10-12)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.2...v0.6.3)
Breaking Changes
----------------
* Add a 'RECONNECTING' state to the sync states. This is an additional state
between 'SYNCING' and 'ERROR', so most clients should not notice.
Other Changes
----------------
* Fix params getting replaced on register calls
[\#233](https://github.com/matrix-org/matrix-js-sdk/pull/233)
* Fix potential 30s delay on reconnect
[\#232](https://github.com/matrix-org/matrix-js-sdk/pull/232)
* uploadContent: Attempt some consistency between browser and node
[\#230](https://github.com/matrix-org/matrix-js-sdk/pull/230)
* Fix error handling on uploadContent
[\#229](https://github.com/matrix-org/matrix-js-sdk/pull/229)
* Fix uploadContent for node.js
[\#226](https://github.com/matrix-org/matrix-js-sdk/pull/226)
* Don't emit ERROR until a keepalive poke fails
[\#223](https://github.com/matrix-org/matrix-js-sdk/pull/223)
* Function to get the fallback url for interactive auth
[\#224](https://github.com/matrix-org/matrix-js-sdk/pull/224)
* Revert "Handle the first /sync failure differently."
[\#222](https://github.com/matrix-org/matrix-js-sdk/pull/222)
Changes in [0.6.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.2) (2016-10-05)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.1...v0.6.2)
* Check dependencies aren't on develop in release.sh
[\#221](https://github.com/matrix-org/matrix-js-sdk/pull/221)
* Fix checkTurnServers leak on logout
[\#220](https://github.com/matrix-org/matrix-js-sdk/pull/220)
* Fix leak of file upload objects
[\#219](https://github.com/matrix-org/matrix-js-sdk/pull/219)
* crypto: remove duplicate code
[\#218](https://github.com/matrix-org/matrix-js-sdk/pull/218)
* Add API for 3rd party location lookup
[\#217](https://github.com/matrix-org/matrix-js-sdk/pull/217)
* Handle the first /sync failure differently.
[\#216](https://github.com/matrix-org/matrix-js-sdk/pull/216)
Changes in [0.6.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.1) (2016-09-21)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.0...v0.6.1)
* Fix the ed25519 key checking
[\#215](https://github.com/matrix-org/matrix-js-sdk/pull/215)
* Add MatrixClient.getEventSenderDeviceInfo()
[\#214](https://github.com/matrix-org/matrix-js-sdk/pull/214)
Changes in [0.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.0) (2016-09-21)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.6...v0.6.0)
* Pull user device list on join
[\#212](https://github.com/matrix-org/matrix-js-sdk/pull/212)
* Fix sending of oh_hais on bad sessions
[\#213](https://github.com/matrix-org/matrix-js-sdk/pull/213)
* Support /publicRooms pagination
[\#211](https://github.com/matrix-org/matrix-js-sdk/pull/211)
* Update the olm library version to 1.3.0
[\#205](https://github.com/matrix-org/matrix-js-sdk/pull/205)
* Comment what the logic in uploadKeys does
[\#209](https://github.com/matrix-org/matrix-js-sdk/pull/209)
* Include keysProved and keysClaimed in the local echo for events we send.
[\#210](https://github.com/matrix-org/matrix-js-sdk/pull/210)
* Check if we need to upload new one-time keys every 10 minutes
[\#208](https://github.com/matrix-org/matrix-js-sdk/pull/208)
* Reset oneTimeKey to null on each loop iteration.
[\#207](https://github.com/matrix-org/matrix-js-sdk/pull/207)
* Add getKeysProved and getKeysClaimed methods to MatrixEvent.
[\#206](https://github.com/matrix-org/matrix-js-sdk/pull/206)
* Send a 'm.new_device' when we get a message for an unknown group session
[\#204](https://github.com/matrix-org/matrix-js-sdk/pull/204)
* Introduce EventTimelineSet, filtered timelines and global notif timeline.
[\#196](https://github.com/matrix-org/matrix-js-sdk/pull/196)
* Wrap the crypto event handlers in try/catch blocks
[\#203](https://github.com/matrix-org/matrix-js-sdk/pull/203)
* Show warnings on to-device decryption fail
[\#202](https://github.com/matrix-org/matrix-js-sdk/pull/202)
* s/Displayname/DisplayName/
[\#201](https://github.com/matrix-org/matrix-js-sdk/pull/201)
* OH HAI
[\#200](https://github.com/matrix-org/matrix-js-sdk/pull/200)
* Share the current ratchet with new members
[\#199](https://github.com/matrix-org/matrix-js-sdk/pull/199)
* Move crypto bits into a subdirectory
[\#198](https://github.com/matrix-org/matrix-js-sdk/pull/198)
* Refactor event handling in Crypto
[\#197](https://github.com/matrix-org/matrix-js-sdk/pull/197)
* Don't create Olm sessions proactively
[\#195](https://github.com/matrix-org/matrix-js-sdk/pull/195)
* Use to-device events for key sharing
[\#194](https://github.com/matrix-org/matrix-js-sdk/pull/194)
* README: callbacks deprecated
[\#193](https://github.com/matrix-org/matrix-js-sdk/pull/193)
* Fix sender verification for megolm messages
[\#192](https://github.com/matrix-org/matrix-js-sdk/pull/192)
* Use `ciphertext` instead of `body` in megolm events
[\#191](https://github.com/matrix-org/matrix-js-sdk/pull/191)
* Add debug methods to get the state of OlmSessions
[\#189](https://github.com/matrix-org/matrix-js-sdk/pull/189)
* MatrixClient.getStoredDevicesForUser
[\#190](https://github.com/matrix-org/matrix-js-sdk/pull/190)
* Olm-related cleanups
[\#188](https://github.com/matrix-org/matrix-js-sdk/pull/188)
* Update to fixed olmlib
[\#187](https://github.com/matrix-org/matrix-js-sdk/pull/187)
* always play audio out of the remoteAudioElement if it exists.
[\#186](https://github.com/matrix-org/matrix-js-sdk/pull/186)
* Fix exceptions where HTMLMediaElement loads and plays race
[\#185](https://github.com/matrix-org/matrix-js-sdk/pull/185)
* Reset megolm session when people join/leave the room
[\#183](https://github.com/matrix-org/matrix-js-sdk/pull/183)
* Fix exceptions when dealing with redactions
[\#184](https://github.com/matrix-org/matrix-js-sdk/pull/184)
Changes in [0.5.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.6) (2016-08-28)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.5...v0.5.6)
* Put all of the megolm keys in one room message
[\#182](https://github.com/matrix-org/matrix-js-sdk/pull/182)
* Reinstate device blocking for simple Olm
[\#181](https://github.com/matrix-org/matrix-js-sdk/pull/181)
* support for unpacking megolm keys
[\#180](https://github.com/matrix-org/matrix-js-sdk/pull/180)
* Send out megolm keys when we start a megolm session
[\#179](https://github.com/matrix-org/matrix-js-sdk/pull/179)
* Change the result structure for ensureOlmSessionsForUsers
[\#178](https://github.com/matrix-org/matrix-js-sdk/pull/178)
* Factor out a function for doing olm encryption
[\#177](https://github.com/matrix-org/matrix-js-sdk/pull/177)
* Move DeviceInfo and DeviceVerification to separate module
[\#175](https://github.com/matrix-org/matrix-js-sdk/pull/175)
* Make encryption asynchronous
[\#176](https://github.com/matrix-org/matrix-js-sdk/pull/176)
* Added ability to set and get status_msg for presence.
[\#167](https://github.com/matrix-org/matrix-js-sdk/pull/167)
* Megolm: don't dereference nullable object
[\#174](https://github.com/matrix-org/matrix-js-sdk/pull/174)
* Implement megolm encryption/decryption
[\#173](https://github.com/matrix-org/matrix-js-sdk/pull/173)
* Update our push rules when they come down stream
[\#170](https://github.com/matrix-org/matrix-js-sdk/pull/170)
* Factor Olm encryption/decryption out to new classes
[\#172](https://github.com/matrix-org/matrix-js-sdk/pull/172)
* Make DeviceInfo more useful, and refactor crypto methods to use it
[\#171](https://github.com/matrix-org/matrix-js-sdk/pull/171)
* Move login and register methods into base-apis
[\#169](https://github.com/matrix-org/matrix-js-sdk/pull/169)
* Remove defaultDeviceDisplayName from MatrixClient options
[\#168](https://github.com/matrix-org/matrix-js-sdk/pull/168)
Changes in [0.5.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.5) (2016-08-11)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.4...v0.5.5)
* Add room.getAliases() and room.getCanonicalAlias
* Add API calls `/register/email/requestToken`, `/account/password/email/requestToken` and `/account/3pid/email/requestToken`
* Add `User.currentlyActive` and `User.lastPresenceTs` events for changes in fields on the User object
* Add `logout` and `deactivateAccount`
* Make sure we actually stop the sync loop on logout
[\#166](https://github.com/matrix-org/matrix-js-sdk/pull/166)
* Zero is a valid power level
[\#164](https://github.com/matrix-org/matrix-js-sdk/pull/164)
* Verify e2e keys on download
[\#163](https://github.com/matrix-org/matrix-js-sdk/pull/163)
* Factor crypto stuff out of MatrixClient
[\#162](https://github.com/matrix-org/matrix-js-sdk/pull/162)
* Refactor device key upload
[\#161](https://github.com/matrix-org/matrix-js-sdk/pull/161)
* Wrappers for devices API
[\#158](https://github.com/matrix-org/matrix-js-sdk/pull/158)
* Add deactivate account function
[\#160](https://github.com/matrix-org/matrix-js-sdk/pull/160)
* client.listDeviceKeys: Expose device display name
[\#159](https://github.com/matrix-org/matrix-js-sdk/pull/159)
* Add `logout`
[\#157](https://github.com/matrix-org/matrix-js-sdk/pull/157)
* Fix email registration
[\#156](https://github.com/matrix-org/matrix-js-sdk/pull/156)
* Factor out MatrixClient methods to MatrixBaseApis
[\#155](https://github.com/matrix-org/matrix-js-sdk/pull/155)
* Fix some broken tests
[\#154](https://github.com/matrix-org/matrix-js-sdk/pull/154)
* make jenkins fail the build if the tests fail
[\#153](https://github.com/matrix-org/matrix-js-sdk/pull/153)
* deviceId-related fixes
[\#152](https://github.com/matrix-org/matrix-js-sdk/pull/152)
* /login, /register: Add device_id and initial_device_display_name
[\#151](https://github.com/matrix-org/matrix-js-sdk/pull/151)
* Support global account_data
[\#150](https://github.com/matrix-org/matrix-js-sdk/pull/150)
* Add more events to User
[\#149](https://github.com/matrix-org/matrix-js-sdk/pull/149)
* Add API calls for other requestToken endpoints
[\#148](https://github.com/matrix-org/matrix-js-sdk/pull/148)
* Add register-specific request token endpoint
[\#147](https://github.com/matrix-org/matrix-js-sdk/pull/147)
* Set a valid SPDX license identifier in package.json
[\#139](https://github.com/matrix-org/matrix-js-sdk/pull/139)
* Configure encryption on m.room.encryption events
[\#145](https://github.com/matrix-org/matrix-js-sdk/pull/145)
* Implement device blocking
[\#146](https://github.com/matrix-org/matrix-js-sdk/pull/146)
* Clearer doc for setRoomDirectoryVisibility
[\#144](https://github.com/matrix-org/matrix-js-sdk/pull/144)
* crypto: use memberlist to derive recipient list
[\#143](https://github.com/matrix-org/matrix-js-sdk/pull/143)
* Support for marking devices as unverified
[\#142](https://github.com/matrix-org/matrix-js-sdk/pull/142)
* Add Olm as an optionalDependency
[\#141](https://github.com/matrix-org/matrix-js-sdk/pull/141)
* Add room.getAliases() and room.getCanonicalAlias()
[\#140](https://github.com/matrix-org/matrix-js-sdk/pull/140)
* Change how MatrixEvent manages encrypted events
[\#138](https://github.com/matrix-org/matrix-js-sdk/pull/138)
* Catch exceptions when encrypting events
[\#137](https://github.com/matrix-org/matrix-js-sdk/pull/137)
* Support for marking devices as verified
[\#136](https://github.com/matrix-org/matrix-js-sdk/pull/136)
* Various matrix-client refactorings and fixes
[\#134](https://github.com/matrix-org/matrix-js-sdk/pull/134)
Changes in [0.5.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.4) (2016-06-02)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.3...v0.5.4)
* Correct fix for https://github.com/vector-im/vector-web/issues/1039
* Make release.sh work on OSX
Changes in [0.5.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.3) (2016-06-02)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.2...v0.5.3)
* Add support for the openid interface
[\#133](https://github.com/matrix-org/matrix-js-sdk/pull/133)
* Bugfix for HTTP upload content when running on node
[\#129](https://github.com/matrix-org/matrix-js-sdk/pull/129)
* Ignore missing profile (displayname and avatar_url) fields on
presence events, rather than overwriting existing valid profile
data from membership events or elsewhere.
Fixes https://github.com/vector-im/vector-web/issues/1039
Changes in [0.5.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.2) (2016-04-19)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.1...v0.5.2)
+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
-5826
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-6490
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-9900
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-10023
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-10864
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-15420
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-15577
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-15785
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-15832
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-16305
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
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 -4
View File
@@ -1,13 +1,33 @@
#!/bin/bash -l
export NVM_DIR="/home/jenkins/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 0.10
npm install
npm test
jshint --reporter=checkstyle -c .jshint lib spec > jshint.xml || echo "jshint finished with return code $?"
gjslint --unix_mode --disable 0131,0211,0200,0222,0212 --max_line_length 90 -r lib/ -r spec/ > gjslint.log || echo "gjslint finished with return code $?"
RC=0
function fail {
echo $@ >&2
RC=1
}
npm test || fail "npm test finished with return code $?"
jshint --reporter=checkstyle -c .jshint lib spec > jshint.xml ||
fail "jshint finished with return code $?"
gjslint --unix_mode --disable 0131,0211,0200,0222,0212 \
--max_line_length 90 \
-r lib/ -r spec/ > gjslint.log ||
fail "gjslint finished with return code $?"
# delete the old tarball, if it exists
rm -f matrix-js-sdk-*.tgz
npm pack
npm pack ||
fail "npm pack finished with return code $?"
npm run gendoc || fail "JSDoc failed with code $?"
exit $RC
+1134
View File
File diff suppressed because it is too large Load Diff
+747 -1262
View File
File diff suppressed because it is too large Load Diff
+744
View File
@@ -0,0 +1,744 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* olm.js wrapper
*
* @module crypto/OlmDevice
*/
var Olm = require("olm");
var utils = require("../utils");
/**
* Manages the olm cryptography functions. Each OlmDevice has a single
* OlmAccount and a number of OlmSessions.
*
* Accounts and sessions are kept pickled in a sessionStore.
*
* @constructor
* @alias module:crypto/OlmDevice
*
* @param {Object} sessionStore A store to be used for data in end-to-end
* crypto
*
* @property {string} deviceCurve25519Key Curve25519 key for the account
* @property {string} deviceEd25519Key Ed25519 key for the account
*/
function OlmDevice(sessionStore) {
this._sessionStore = sessionStore;
this._pickleKey = "DEFAULT_KEY";
var e2eKeys;
var account = new Olm.Account();
try {
_initialise_account(this._sessionStore, this._pickleKey, account);
e2eKeys = JSON.parse(account.identity_keys());
} finally {
account.free();
}
this.deviceCurve25519Key = e2eKeys.curve25519;
this.deviceEd25519Key = e2eKeys.ed25519;
// we don't bother stashing outboundgroupsessions in the sessionstore -
// instead we keep them here.
this._outboundGroupSessionStore = {};
// Store a set of decrypted message indexes for each group session.
// This partially mitigates a replay attack where a MITM resends a group
// message into the room.
//
// TODO: If we ever remove an event from memory we will also need to remove
// it from this map. Otherwise if we download the event from the server we
// will think that it is a duplicate.
//
// Keys are strings of form "<senderKey>|<session_id>|<message_index>"
// Values are true.
this._inboundGroupSessionMessageIndexes = {};
}
function _initialise_account(sessionStore, pickleKey, account) {
var e2eAccount = sessionStore.getEndToEndAccount();
if (e2eAccount !== null) {
account.unpickle(pickleKey, e2eAccount);
return;
}
account.create();
var pickled = account.pickle(pickleKey);
sessionStore.storeEndToEndAccount(pickled);
}
/**
* @return {array} The version of Olm.
*/
OlmDevice.getOlmVersion = function() {
return Olm.get_library_version();
};
/**
* extract our OlmAccount from the session store and call the given function
*
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getAccount = function(func) {
var account = new Olm.Account();
try {
var pickledAccount = this._sessionStore.getEndToEndAccount();
account.unpickle(this._pickleKey, pickledAccount);
return func(account);
} finally {
account.free();
}
};
/**
* store our OlmAccount in the session store
*
* @param {OlmAccount} account
* @private
*/
OlmDevice.prototype._saveAccount = function(account) {
var pickledAccount = account.pickle(this._pickleKey);
this._sessionStore.storeEndToEndAccount(pickledAccount);
};
/**
* extract an OlmSession from the session store and call the given function
*
* @param {string} deviceKey
* @param {string} sessionId
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getSession = function(deviceKey, sessionId, func) {
var sessions = this._sessionStore.getEndToEndSessions(deviceKey);
var pickledSession = sessions[sessionId];
var session = new Olm.Session();
try {
session.unpickle(this._pickleKey, pickledSession);
return func(session);
} finally {
session.free();
}
};
/**
* store our OlmSession in the session store
*
* @param {string} deviceKey
* @param {OlmSession} session
* @private
*/
OlmDevice.prototype._saveSession = function(deviceKey, session) {
var pickledSession = session.pickle(this._pickleKey);
this._sessionStore.storeEndToEndSession(
deviceKey, session.session_id(), pickledSession
);
};
/**
* get an OlmUtility and call the given function
*
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getUtility = function(func) {
var utility = new Olm.Utility();
try {
return func(utility);
} finally {
utility.free();
}
};
/**
* Signs a message with the ed25519 key for this account.
*
* @param {string} message message to be signed
* @return {string} base64-encoded signature
*/
OlmDevice.prototype.sign = function(message) {
return this._getAccount(function(account) {
return account.sign(message);
});
};
/**
* Get the current (unused, unpublished) one-time keys for this account.
*
* @return {object} one time keys; an object with the single property
* <tt>curve25519</tt>, which is itself an object mapping key id to Curve25519
* key.
*/
OlmDevice.prototype.getOneTimeKeys = function() {
return this._getAccount(function(account) {
return JSON.parse(account.one_time_keys());
});
};
/**
* Get the maximum number of one-time keys we can store.
*
* @return {number} number of keys
*/
OlmDevice.prototype.maxNumberOfOneTimeKeys = function() {
return this._getAccount(function(account) {
return account.max_number_of_one_time_keys();
});
};
/**
* Marks all of the one-time keys as published.
*/
OlmDevice.prototype.markKeysAsPublished = function() {
var self = this;
this._getAccount(function(account) {
account.mark_keys_as_published();
self._saveAccount(account);
});
};
/**
* Generate some new one-time keys
*
* @param {number} numKeys number of keys to generate
*/
OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
var self = this;
this._getAccount(function(account) {
account.generate_one_time_keys(numKeys);
self._saveAccount(account);
});
};
/**
* Generate a new outbound session
*
* The new session will be stored in the sessionStore.
*
* @param {string} theirIdentityKey remote user's Curve25519 identity key
* @param {string} theirOneTimeKey remote user's one-time Curve25519 key
* @return {string} sessionId for the outbound session.
*/
OlmDevice.prototype.createOutboundSession = function(
theirIdentityKey, theirOneTimeKey
) {
var self = this;
return this._getAccount(function(account) {
var session = new Olm.Session();
try {
session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
self._saveSession(theirIdentityKey, session);
return session.session_id();
} finally {
session.free();
}
});
};
/**
* Generate a new inbound session, given an incoming message
*
* @param {string} theirDeviceIdentityKey remote user's Curve25519 identity key
* @param {number} message_type message_type field from the received message (must be 0)
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {{payload: string, session_id: string}} decrypted payload, and
* session id of new session
*
* @raises {Error} if the received message was not valid (for instance, it
* didn't use a valid one-time key).
*/
OlmDevice.prototype.createInboundSession = function(
theirDeviceIdentityKey, message_type, ciphertext
) {
if (message_type !== 0) {
throw new Error("Need message_type == 0 to create inbound session");
}
var self = this;
return this._getAccount(function(account) {
var session = new Olm.Session();
try {
session.create_inbound_from(account, theirDeviceIdentityKey, ciphertext);
account.remove_one_time_keys(session);
self._saveAccount(account);
var payloadString = session.decrypt(message_type, ciphertext);
self._saveSession(theirDeviceIdentityKey, session);
return {
payload: payloadString,
session_id: session.session_id(),
};
} finally {
session.free();
}
});
};
/**
* Get a list of known session IDs for the given device
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @return {string[]} a list of known session ids for the device
*/
OlmDevice.prototype.getSessionIdsForDevice = function(theirDeviceIdentityKey) {
var sessions = this._sessionStore.getEndToEndSessions(
theirDeviceIdentityKey
);
return utils.keys(sessions);
};
/**
* Get the right olm session id for encrypting messages to the given identity key
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @return {string?} session id, or null if no established session
*/
OlmDevice.prototype.getSessionIdForDevice = function(theirDeviceIdentityKey) {
var sessionIds = this.getSessionIdsForDevice(theirDeviceIdentityKey);
if (sessionIds.length === 0) {
return null;
}
// Use the session with the lowest ID.
sessionIds.sort();
return sessionIds[0];
};
/**
* Get information on the active Olm sessions for a device.
* <p>
* Returns an array, with an entry for each active session. The first entry in
* the result will be the one used for outgoing messages. Each entry contains
* the keys 'hasReceivedMessage' (true if the session has received an incoming
* message and is therefore past the pre-key stage), and 'sessionId'.
*
* @param {string} deviceIdentityKey Curve25519 identity key for the device
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
*/
OlmDevice.prototype.getSessionInfoForDevice = function(deviceIdentityKey) {
var sessionIds = this.getSessionIdsForDevice(deviceIdentityKey);
sessionIds.sort();
var info = [];
function getSessionInfo(session) {
return {
hasReceivedMessage: session.has_received_message()
};
}
for (var i = 0; i < sessionIds.length; i++) {
var sessionId = sessionIds[i];
var res = this._getSession(deviceIdentityKey, sessionId, getSessionInfo);
res.sessionId = sessionId;
info.push(res);
}
return info;
};
/**
* Encrypt an outgoing message using an existing session
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {string} payloadString payload to be encrypted and sent
*
* @return {string} ciphertext
*/
OlmDevice.prototype.encryptMessage = function(
theirDeviceIdentityKey, sessionId, payloadString
) {
var self = this;
if (payloadString === undefined) {
throw new Error("payloadString undefined");
}
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
var res = session.encrypt(payloadString);
self._saveSession(theirDeviceIdentityKey, session);
return res;
});
};
/**
* Decrypt an incoming message using an existing session
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {number} message_type message_type field from the received message
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {string} decrypted payload.
*/
OlmDevice.prototype.decryptMessage = function(
theirDeviceIdentityKey, sessionId, message_type, ciphertext
) {
var self = this;
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
var payloadString = session.decrypt(message_type, ciphertext);
self._saveSession(theirDeviceIdentityKey, session);
return payloadString;
});
};
/**
* Determine if an incoming messages is a prekey message matching an existing session
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {number} message_type message_type field from the received message
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {boolean} true if the received message is a prekey message which matches
* the given session.
*/
OlmDevice.prototype.matchesSession = function(
theirDeviceIdentityKey, sessionId, message_type, ciphertext
) {
if (message_type !== 0) {
return false;
}
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
return session.matches_inbound(ciphertext);
});
};
// Outbound group session
// ======================
/**
* store an OutboundGroupSession in _outboundGroupSessionStore
*
* @param {Olm.OutboundGroupSession} session
* @private
*/
OlmDevice.prototype._saveOutboundGroupSession = function(session) {
var pickledSession = session.pickle(this._pickleKey);
this._outboundGroupSessionStore[session.session_id()] = pickledSession;
};
/**
* extract an OutboundGroupSession from _outboundGroupSessionStore and call the
* given function
*
* @param {string} sessionId
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
var pickled = this._outboundGroupSessionStore[sessionId];
if (pickled === null) {
throw new Error("Unknown outbound group session " + sessionId);
}
var session = new Olm.OutboundGroupSession();
try {
session.unpickle(this._pickleKey, pickled);
return func(session);
} finally {
session.free();
}
};
/**
* Generate a new outbound group session
*
* @return {string} sessionId for the outbound session.
*/
OlmDevice.prototype.createOutboundGroupSession = function() {
var session = new Olm.OutboundGroupSession();
try {
session.create();
this._saveOutboundGroupSession(session);
return session.session_id();
} finally {
session.free();
}
};
/**
* Encrypt an outgoing message with an outbound group session
*
* @param {string} sessionId the id of the outboundgroupsession
* @param {string} payloadString payload to be encrypted and sent
*
* @return {string} ciphertext
*/
OlmDevice.prototype.encryptGroupMessage = function(sessionId, payloadString) {
var self = this;
return this._getOutboundGroupSession(sessionId, function(session) {
var res = session.encrypt(payloadString);
self._saveOutboundGroupSession(session);
return res;
});
};
/**
* Get the session keys for an outbound group session
*
* @param {string} sessionId the id of the outbound group session
*
* @return {{chain_index: number, key: string}} current chain index, and
* base64-encoded secret key.
*/
OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
return this._getOutboundGroupSession(sessionId, function(session) {
return {
chain_index: session.message_index(),
key: session.session_key(),
};
});
};
// Inbound group session
// =====================
/**
* store an InboundGroupSession in the session store
*
* @param {string} roomId
* @param {string} senderCurve25519Key
* @param {string} sessionId
* @param {Olm.InboundGroupSession} session
* @param {object} keysClaimed Other keys the sender claims.
* @private
*/
OlmDevice.prototype._saveInboundGroupSession = function(
roomId, senderCurve25519Key, sessionId, session, keysClaimed
) {
var r = {
room_id: roomId,
session: session.pickle(this._pickleKey),
keysClaimed: keysClaimed,
};
this._sessionStore.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, JSON.stringify(r)
);
};
/**
* extract an InboundGroupSession from the session store and call the given function
*
* @param {string} roomId
* @param {string} senderKey
* @param {string} sessionId
* @param {function(Olm.InboundGroupSession, Object<string, string>): T} func
* function to call. Second argument is the map of keys claimed by the session.
*
* @return {null} the sessionId is unknown
*
* @return {T} result of func
*
* @private
* @template {T}
*/
OlmDevice.prototype._getInboundGroupSession = function(
roomId, senderKey, sessionId, func
) {
var r = this._sessionStore.getEndToEndInboundGroupSession(
senderKey, sessionId
);
if (r === null) {
return null;
}
r = JSON.parse(r);
// check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room.
if (roomId !== r.room_id) {
throw new Error(
"Mismatched room_id for inbound group session (expected " + r.room_id +
", was " + roomId + ")"
);
}
var session = new Olm.InboundGroupSession();
try {
session.unpickle(this._pickleKey, r.session);
return func(session, r.keysClaimed || {});
} finally {
session.free();
}
};
/**
* Add an inbound group session to the session store
*
* @param {string} roomId room in which this session will be used
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {string} sessionKey base64-encoded secret key
* @param {Object<string, string>} keysClaimed Other keys the sender claims.
*/
OlmDevice.prototype.addInboundGroupSession = function(
roomId, senderKey, sessionId, sessionKey, keysClaimed
) {
var self = this;
/* if we already have this session, consider updating it */
function updateSession(session) {
console.log("Update for megolm session " + senderKey + "/" + sessionId);
// for now we just ignore updates. TODO: implement something here
return true;
}
var r = this._getInboundGroupSession(
roomId, senderKey, sessionId, updateSession
);
if (r !== null) {
return;
}
// new session.
var session = new Olm.InboundGroupSession();
try {
session.create(sessionKey);
if (sessionId != session.session_id()) {
throw new Error(
"Mismatched group session ID from senderKey: " + senderKey
);
}
self._saveInboundGroupSession(
roomId, senderKey, sessionId, session, keysClaimed
);
} finally {
session.free();
}
};
/**
* Decrypt a received message with an inbound group session
*
* @param {string} roomId room in which the message was received
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {string} body base64-encoded body of the encrypted message
*
* @return {null} the sessionId is unknown
*
* @return {{result: string, keysProved: Object<string, string>, keysClaimed:
* Object<string, string>}} result
*/
OlmDevice.prototype.decryptGroupMessage = function(
roomId, senderKey, sessionId, body
) {
var self = this;
function decrypt(session, keysClaimed) {
var res = session.decrypt(body);
var plaintext = res.plaintext;
if (plaintext === undefined) {
// Compatibility for older olm versions.
plaintext = res;
} else {
// Check if we have seen this message index before to detect replay attacks.
var messageIndexKey = senderKey + "|" + sessionId + "|" + res.message_index;
if (messageIndexKey in self._inboundGroupSessionMessageIndexes) {
throw new Error(
"Duplicate message index, possible replay attack: " +
messageIndexKey
);
}
self._inboundGroupSessionMessageIndexes[messageIndexKey] = true;
}
// the sender must have had the senderKey to persuade us to save the
// session.
var keysProved = {curve25519: senderKey};
self._saveInboundGroupSession(
roomId, senderKey, sessionId, session, keysClaimed
);
return {
result: plaintext,
keysClaimed: keysClaimed,
keysProved: keysProved,
};
}
return this._getInboundGroupSession(
roomId, senderKey, sessionId, decrypt
);
};
// Utilities
// =========
/**
* Verify an ed25519 signature.
*
* @param {string} key ed25519 key
* @param {string} message message which was signed
* @param {string} signature base64-encoded signature to be checked
*
* @raises {Error} if there is a problem with the verification. If the key was
* too small then the message will be "OLM.INVALID_BASE64". If the signature
* was invalid then the message will be "OLM.BAD_MESSAGE_MAC".
*/
OlmDevice.prototype.verifySignature = function(
key, message, signature
) {
this._getUtility(function(util) {
util.ed25519_verify(key, message, signature);
});
};
/** */
module.exports = OlmDevice;
+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;
+537
View File
@@ -0,0 +1,537 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* Defines m.olm encryption/decryption
*
* @module crypto/algorithms/megolm
*/
var q = require("q");
var utils = require("../../utils");
var olmlib = require("../olmlib");
var base = require("./base");
/**
* @private
* @constructor
*
* @param {string} sessionId
*
* @property {string} sessionId
* @property {Number} useCount number of times this session has been used
* @property {Number} creationTime when the session was created (ms since the epoch)
* @property {module:client.Promise?} sharePromise If a share operation is in progress,
* a promise which resolves when it is complete.
*
* @property {object} sharedWithDevices
* devices with which we have shared the session key
* userId -> {deviceId -> msgindex}
*/
function OutboundSessionInfo(sessionId) {
this.sessionId = sessionId;
this.useCount = 0;
this.creationTime = new Date().getTime();
this.sharePromise = null;
this.sharedWithDevices = {};
}
/**
* Check if it's time to rotate the session
*
* @param {Number} rotationPeriodMsgs
* @param {Number} rotationPeriodMs
* @return {Boolean}
*/
OutboundSessionInfo.prototype.needsRotation = function(
rotationPeriodMsgs, rotationPeriodMs
) {
var sessionLifetime = new Date().getTime() - this.creationTime;
if (this.useCount >= rotationPeriodMsgs ||
sessionLifetime >= rotationPeriodMs
) {
console.log(
"Rotating megolm session after " + this.useCount +
" messages, " + sessionLifetime + "ms"
);
return true;
}
return false;
};
/**
* Megolm encryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.EncryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.EncryptionAlgorithm}
*/
function MegolmEncryption(params) {
base.EncryptionAlgorithm.call(this, params);
// OutboundSessionInfo. Null if we haven't yet started setting one up. Note
// that even if this is non-null, it may not be ready for use (in which
// case _outboundSession.sharePromise will be non-null.)
this._outboundSession = null;
// default rotation periods
this._sessionRotationPeriodMsgs = 100;
this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
if (params.config.rotation_period_ms !== undefined) {
this._sessionRotationPeriodMs = params.config.rotation_period_ms;
}
if (params.config.rotation_period_msgs !== undefined) {
this._sessionRotationPeriodMsgs = params.config.rotation_period_msgs;
}
}
utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
/**
* @private
*
* @param {module:models/room} room
*
* @return {module:client.Promise} Promise which resolves to the
* OutboundSessionInfo when setup is complete.
*/
MegolmEncryption.prototype._ensureOutboundSession = function(room) {
var self = this;
var session = this._outboundSession;
// need to make a brand new session?
if (!session || session.needsRotation(self._sessionRotationPeriodMsgs,
self._sessionRotationPeriodMs)
) {
this._outboundSession = session = this._prepareNewSession(room);
}
if (session.sharePromise) {
// key share already in progress
return session.sharePromise;
}
// no share in progress: check if we need to share with any devices
var prom = this._getDevicesInRoom(room).then(function(devicesInRoom) {
var shareMap = {};
for (var userId in devicesInRoom) {
if (!devicesInRoom.hasOwnProperty(userId)) {
continue;
}
var userDevices = devicesInRoom[userId];
for (var deviceId in userDevices) {
if (!userDevices.hasOwnProperty(deviceId)) {
continue;
}
var deviceInfo = userDevices[deviceId];
if (deviceInfo.isBlocked()) {
continue;
}
var key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
continue;
}
if (
!session.sharedWithDevices[userId] ||
session.sharedWithDevices[userId][deviceId] === undefined
) {
shareMap[userId] = shareMap[userId] || [];
shareMap[userId].push(deviceInfo);
}
}
}
return self._shareKeyWithDevices(
session, shareMap
);
}).finally(function() {
session.sharePromise = null;
}).then(function() {
return session;
});
session.sharePromise = prom;
return prom;
};
/**
* @private
*
* @param {module:models/room} room
*
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*/
MegolmEncryption.prototype._prepareNewSession = function(room) {
var session_id = this._olmDevice.createOutboundGroupSession();
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
this._olmDevice.addInboundGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, session_id,
key.key, {ed25519: this._olmDevice.deviceEd25519Key}
);
return new OutboundSessionInfo(session_id);
};
/**
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @return {module:client.Promise} Promise which resolves once the key sharing
* message has been sent.
*/
MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUser) {
var self = this;
var key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
var payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: session.sessionId,
session_key: key.key,
chain_index: key.chain_index,
}
};
var contentMap = {};
return olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser
).then(function(devicemap) {
var haveTargets = false;
for (var userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
var devicesToShareWith = devicesByUser[userId];
var sessionResults = devicemap[userId];
for (var i = 0; i < devicesToShareWith.length; i++) {
var deviceInfo = devicesToShareWith[i];
var deviceId = deviceInfo.deviceId;
var sessionResult = sessionResults[deviceId];
if (!sessionResult.sessionId) {
// no session with this device, probably because there
// were no one-time keys.
//
// we could send them a to_device message anyway, as a
// signal that they have missed out on the key sharing
// message because of the lack of keys, but there's not
// much point in that really; it will mostly serve to clog
// up to_device inboxes.
//
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
continue;
}
console.log(
"sharing keys with device " + userId + ":" + deviceId
);
var encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
self._userId,
self._deviceId,
self._olmDevice,
userId,
deviceInfo,
payload
);
if (!contentMap[userId]) {
contentMap[userId] = {};
}
contentMap[userId][deviceId] = encryptedContent;
haveTargets = true;
}
}
if (!haveTargets) {
return q();
}
// TODO: retries
return self._baseApis.sendToDevice("m.room.encrypted", contentMap);
}).then(function() {
// Add the devices we have shared with to session.sharedWithDevices.
//
// we deliberately iterate over devicesByUser (ie, the devices we
// attempted to share with) rather than the contentMap (those we did
// share with), because we don't want to try to claim a one-time-key
// for dead devices on every message.
for (var userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
if (!session.sharedWithDevices[userId]) {
session.sharedWithDevices[userId] = {};
}
var devicesToShareWith = devicesByUser[userId];
for (var i = 0; i < devicesToShareWith.length; i++) {
var deviceInfo = devicesToShareWith[i];
session.sharedWithDevices[userId][deviceInfo.deviceId] =
key.chain_index;
}
}
});
};
/**
* @inheritdoc
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
var self = this;
return this._ensureOutboundSession(room).then(function(session) {
var payloadJson = {
room_id: self._roomId,
type: eventType,
content: content
};
var ciphertext = self._olmDevice.encryptGroupMessage(
session.sessionId, JSON.stringify(payloadJson)
);
var encryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext,
session_id: session.sessionId,
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
device_id: self._deviceId,
};
session.useCount++;
return encryptedContent;
});
};
/**
* @inheritdoc
*
* @param {module:models/event.MatrixEvent} event event causing the change
* @param {module:models/room-member} member user whose membership changed
* @param {string=} oldMembership previous membership
*/
MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembership) {
var newMembership = member.membership;
if (newMembership === 'join' || newMembership === 'invite') {
return;
}
// otherwise we assume the user is leaving, and start a new outbound session.
console.log("Discarding outbound megolm session due to change in " +
"membership of " + member.userId + " (" + oldMembership +
"->" + newMembership + ")");
// this ensures that we will start a new session on the next message.
this._outboundSession = null;
};
/**
* Get the list of devices for all users in the room
*
* @param {module:models/room} room
*
* @return {module:client.Promise} Promise which resolves to a map
* from userId to deviceId to deviceInfo
*/
MegolmEncryption.prototype._getDevicesInRoom = function(room) {
// XXX what about rooms where invitees can see the content?
var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
// We are happy to use a cached version here: we assume that if we already
// have a list of the user's devices, then we already share an e2e room
// with them, which means that they will have announced any new devices via
// an m.new_device.
return this._crypto.downloadKeys(roomMembers, false);
};
/**
* Megolm decryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.DecryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.DecryptionAlgorithm}
*/
function MegolmDecryption(params) {
base.DecryptionAlgorithm.call(this, params);
// events which we couldn't decrypt due to unknown sessions / indexes: map from
// senderKey|sessionId to list of MatrixEvents
this._pendingEvents = {};
}
utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
/**
* @inheritdoc
*
* @param {MatrixEvent} event
*
* @return {null} The event referred to an unknown megolm session
* @return {module:crypto.DecryptionResult} decryption result
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
MegolmDecryption.prototype.decryptEvent = function(event) {
var content = event.getWireContent();
if (!content.sender_key || !content.session_id ||
!content.ciphertext
) {
throw new base.DecryptionError("Missing fields in input");
}
var res;
try {
res = this._olmDevice.decryptGroupMessage(
event.getRoomId(), content.sender_key, content.session_id, content.ciphertext
);
} catch (e) {
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
this._addEventToPendingList(event);
}
throw new base.DecryptionError(e);
}
if (res === null) {
// We've got a message for a session we don't have.
this._addEventToPendingList(event);
throw new base.DecryptionError("Unknown inbound session id");
}
var payload = JSON.parse(res.result);
// belt-and-braces check that the room id matches that indicated by the HS
// (this is somewhat redundant, since the megolm session is scoped to the
// room, so neither the sender nor a MITM can lie about the room_id).
if (payload.room_id !== event.getRoomId()) {
throw new base.DecryptionError(
"Message intended for room " + payload.room_id
);
}
event.setClearData(payload, res.keysProved, res.keysClaimed);
};
/**
* Add an event to the list of those we couldn't decrypt the first time we
* saw them.
*
* @private
*
* @param {module:models/event.MatrixEvent} event
*/
MegolmDecryption.prototype._addEventToPendingList = function(event) {
var content = event.getWireContent();
var k = content.sender_key + "|" + content.session_id;
if (!this._pendingEvents[k]) {
this._pendingEvents[k] = [];
}
this._pendingEvents[k].push(event);
};
/**
* @inheritdoc
*
* @param {module:models/event.MatrixEvent} event key event
*/
MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
console.log("Adding key from ", event);
var content = event.getContent();
if (!content.room_id ||
!content.session_id ||
!content.session_key
) {
console.error("key event is missing fields");
return;
}
this._olmDevice.addInboundGroupSession(
content.room_id, event.getSenderKey(), content.session_id,
content.session_key, event.getKeysClaimed()
);
var k = event.getSenderKey() + "|" + content.session_id;
var pending = this._pendingEvents[k];
if (pending) {
// have another go at decrypting events sent with this session.
delete this._pendingEvents[k];
for (var i = 0; i < pending.length; i++) {
try {
this.decryptEvent(pending[i]);
console.log("successful re-decryption of", pending[i]);
} catch (e) {
console.log("Still can't decrypt", pending[i], e.stack || e);
}
}
}
};
base.registerAlgorithm(
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption
);
+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;
+1259
View File
File diff suppressed because it is too large Load Diff
+268
View File
@@ -0,0 +1,268 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module olmlib
*
* Utilities common to olm encryption algorithms
*/
var q = require('q');
var anotherjson = require('another-json');
var utils = require("../utils");
/**
* matrix algorithm tag for olm
*/
module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
/**
* matrix algorithm tag for megolm
*/
module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
/**
* Encrypt an event payload for an Olm device
*
* @param {Object<string, string>} resultsObject The `ciphertext` property
* of the m.room.encrypted event to which to add our result
*
* @param {string} ourUserId
* @param {string} ourDeviceId
* @param {module:crypto/OlmDevice} olmDevice olm.js wrapper
* @param {string} recipientUserId
* @param {module:crypto/deviceinfo} recipientDevice
* @param {object} payloadFields fields to include in the encrypted payload
*/
module.exports.encryptMessageForDevice = function(
resultsObject,
ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice,
payloadFields
) {
var deviceKey = recipientDevice.getIdentityKey();
var sessionId = olmDevice.getSessionIdForDevice(deviceKey);
if (sessionId === null) {
// If we don't have a session for a device then
// we can't encrypt a message for it.
return;
}
console.log(
"Using sessionid " + sessionId + " for device " +
recipientUserId + ":" + recipientDevice.deviceId
);
var payload = {
sender: ourUserId,
sender_device: ourDeviceId,
// Include the Ed25519 key so that the recipient knows what
// device this message came from.
// We don't need to include the curve25519 key since the
// recipient will already know this from the olm headers.
// When combined with the device keys retrieved from the
// homeserver signed by the ed25519 key this proves that
// the curve25519 key and the ed25519 key are owned by
// the same device.
keys: {
"ed25519": olmDevice.deviceEd25519Key,
},
// include the recipient device details in the payload,
// to avoid unknown key attacks, per
// https://github.com/vector-im/vector-web/issues/2483
recipient: recipientUserId,
recipient_keys: {
"ed25519": recipientDevice.getFingerprint(),
},
};
// TODO: technically, a bunch of that stuff only needs to be included for
// pre-key messages: after that, both sides know exactly which devices are
// involved in the session. If we're looking to reduce data transfer in the
// future, we could elide them for subsequent messages.
utils.extend(payload, payloadFields);
resultsObject[deviceKey] = olmDevice.encryptMessage(
deviceKey, sessionId, JSON.stringify(payload)
);
};
/**
* Try to make sure we have established olm sessions for the given devices.
*
* @param {module:crypto/OlmDevice} olmDevice
*
* @param {module:base-apis~MatrixBaseApis} baseApis
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @return {module:client.Promise} resolves once the sessions are complete, to
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
*/
module.exports.ensureOlmSessionsForDevices = function(
olmDevice, baseApis, devicesByUser
) {
var devicesWithoutSession = [
// [userId, deviceId], ...
];
var result = {};
for (var userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) { continue; }
result[userId] = {};
var devices = devicesByUser[userId];
for (var j = 0; j < devices.length; j++) {
var deviceInfo = devices[j];
var deviceId = deviceInfo.deviceId;
var key = deviceInfo.getIdentityKey();
var sessionId = olmDevice.getSessionIdForDevice(key);
if (sessionId === null) {
devicesWithoutSession.push([userId, deviceId]);
}
result[userId][deviceId] = {
device: deviceInfo,
sessionId: sessionId,
};
}
}
if (devicesWithoutSession.length === 0) {
return q(result);
}
// TODO: this has a race condition - if we try to send another message
// while we are claiming a key, we will end up claiming two and setting up
// two sessions.
//
// That should eventually resolve itself, but it's poor form.
var oneTimeKeyAlgorithm = "signed_curve25519";
return baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm
).then(function(res) {
for (var userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) { continue; }
var userRes = res.one_time_keys[userId] || {};
var devices = devicesByUser[userId];
for (var j = 0; j < devices.length; j++) {
var deviceInfo = devices[j];
var deviceId = deviceInfo.deviceId;
if (result[userId][deviceId].sessionId) {
// we already have a result for this device
continue;
}
var deviceRes = userRes[deviceId] || {};
var oneTimeKey = null;
for (var keyId in deviceRes) {
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
oneTimeKey = deviceRes[keyId];
}
}
if (!oneTimeKey) {
console.warn(
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
") for device " + userId + ":" + deviceId
);
continue;
}
var sid = _verifyKeyAndStartSession(
olmDevice, oneTimeKey, userId, deviceInfo
);
result[userId][deviceId].sessionId = sid;
}
}
return result;
});
};
function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
var deviceId = deviceInfo.deviceId;
try {
_verifySignature(
olmDevice, oneTimeKey, userId, deviceId,
deviceInfo.getFingerprint()
);
} catch (e) {
console.error(
"Unable to verify signature on one-time key for device " +
userId + ":" + deviceId + ":", e
);
return null;
}
var sid;
try {
sid = olmDevice.createOutboundSession(
deviceInfo.getIdentityKey(), oneTimeKey.key
);
} catch (e) {
// possibly a bad key
console.error("Error starting session with device " +
userId + ":" + deviceId + ": " + e);
return null;
}
console.log("Started new sessionid " + sid +
" for device " + userId + ":" + deviceId);
return sid;
}
/**
* Verify the signature on an object
*
* @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op
*
* @param {Object} obj object to check signature on. Note that this will be
* stripped of its 'signatures' and 'unsigned' properties.
*
* @param {string} signingUserId ID of the user whose signature should be checked
*
* @param {string} signingDeviceId ID of the device whose signature should be checked
*
* @param {string} signingKey base64-ed ed25519 public key
*/
var _verifySignature = module.exports.verifySignature = function(
olmDevice, obj, signingUserId, signingDeviceId, signingKey
) {
var signKeyId = "ed25519:" + signingDeviceId;
var signatures = obj.signatures || {};
var userSigs = signatures[signingUserId] || {};
var signature = userSigs[signKeyId];
if (!signature) {
throw Error("No signature");
}
// prepare the canonical json: remove unsigned and signatures, and stringify with
// anotherjson
delete obj.unsigned;
delete obj.signatures;
var json = anotherjson.stringify(obj);
olmDevice.verifySignature(
signingKey, json, signature
);
};
+141
View File
@@ -0,0 +1,141 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module filter-component
*/
/**
* Checks if a value matches a given field value, which may be a * terminated
* wildcard pattern.
* @param {String} actual_value The value to be compared
* @param {String} filter_value The filter pattern to be compared
* @return {bool} true if the actual_value matches the filter_value
*/
function _matches_wildcard(actual_value, filter_value) {
if (filter_value.endsWith("*")) {
var type_prefix = filter_value.slice(0, -1);
return actual_value.substr(0, type_prefix.length) === type_prefix;
}
else {
return actual_value === filter_value;
}
}
/**
* FilterComponent is a section of a Filter definition which defines the
* types, rooms, senders filters etc to be applied to a particular type of resource.
* This is all ported over from synapse's Filter object.
*
* N.B. that synapse refers to these as 'Filters', and what js-sdk refers to as
* 'Filters' are referred to as 'FilterCollections'.
*
* @constructor
* @param {Object} the definition of this filter JSON, e.g. { 'contains_url': true }
*/
function FilterComponent(filter_json) {
this.filter_json = filter_json;
this.types = filter_json.types || null;
this.not_types = filter_json.not_types || [];
this.rooms = filter_json.rooms || null;
this.not_rooms = filter_json.not_rooms || [];
this.senders = filter_json.senders || null;
this.not_senders = filter_json.not_senders || [];
this.contains_url = filter_json.contains_url || null;
}
/**
* Checks with the filter component matches the given event
* @param {MatrixEvent} event event to be checked against the filter
* @return {bool} true if the event matches the filter
*/
FilterComponent.prototype.check = function(event) {
return this._checkFields(
event.getRoomId(),
event.getSender(),
event.getType(),
event.getContent() ? event.getContent().url !== undefined : false
);
};
/**
* Checks whether the filter component matches the given event fields.
* @param {String} room_id the room_id for the event being checked
* @param {String} sender the sender of the event being checked
* @param {String} event_type the type of the event being checked
* @param {String} contains_url whether the event contains a content.url field
* @return {bool} true if the event fields match the filter
*/
FilterComponent.prototype._checkFields =
function(room_id, sender, event_type, contains_url)
{
var literal_keys = {
"rooms": function(v) { return room_id === v; },
"senders": function(v) { return sender === v; },
"types": function(v) { return _matches_wildcard(event_type, v); },
};
var self = this;
Object.keys(literal_keys).forEach(function(name) {
var match_func = literal_keys[name];
var not_name = "not_" + name;
var disallowed_values = self[not_name];
if (disallowed_values.map(match_func)) {
return false;
}
var allowed_values = self[name];
if (allowed_values) {
if (!allowed_values.map(match_func)) {
return false;
}
}
});
var contains_url_filter = this.filter_json.contains_url;
if (contains_url_filter !== undefined) {
if (contains_url_filter !== contains_url) {
return false;
}
}
return true;
};
/**
* Filters a list of events down to those which match this filter component
* @param {MatrixEvent[]} events Events to be checked againt the filter component
* @return {MatrixEvent[]} events which matched the filter component
*/
FilterComponent.prototype.filter = function(events) {
return events.filter(this.check, this);
};
/**
* Returns the limit field for a given filter component, providing a default of
* 10 if none is otherwise specified. Cargo-culted from Synapse.
* @return {Number} the limit for this filter component.
*/
FilterComponent.prototype.limit = function() {
return this.filter_json.limit !== undefined ? this.filter_json.limit : 10;
};
/** The FilterComponent class */
module.exports = FilterComponent;
+92
View File
@@ -18,6 +18,8 @@ limitations under the License.
* @module filter
*/
var FilterComponent = require("./filter-component");
/**
* @param {Object} obj
* @param {string} keyNesting
@@ -49,6 +51,14 @@ function Filter(userId, filterId) {
this.definition = {};
}
/**
* Get the ID of this filter on your homeserver (if known)
* @return {?Number} The filter ID
*/
Filter.prototype.getFilterId = function() {
return this.filterId;
};
/**
* Get the JSON body of the filter.
* @return {Object} The filter definition
@@ -63,6 +73,88 @@ Filter.prototype.getDefinition = function() {
*/
Filter.prototype.setDefinition = function(definition) {
this.definition = definition;
// This is all ported from synapse's FilterCollection()
// definitions look something like:
// {
// "room": {
// "rooms": ["!abcde:example.com"],
// "not_rooms": ["!123456:example.com"],
// "state": {
// "types": ["m.room.*"],
// "not_rooms": ["!726s6s6q:example.com"],
// },
// "timeline": {
// "limit": 10,
// "types": ["m.room.message"],
// "not_rooms": ["!726s6s6q:example.com"],
// "not_senders": ["@spam:example.com"]
// "contains_url": true
// },
// "ephemeral": {
// "types": ["m.receipt", "m.typing"],
// "not_rooms": ["!726s6s6q:example.com"],
// "not_senders": ["@spam:example.com"]
// }
// },
// "presence": {
// "types": ["m.presence"],
// "not_senders": ["@alice:example.com"]
// },
// "event_format": "client",
// "event_fields": ["type", "content", "sender"]
// }
var room_filter_json = definition.room;
// consider the top level rooms/not_rooms filter
var room_filter_fields = {};
if (room_filter_json) {
if (room_filter_json.rooms) {
room_filter_fields.rooms = room_filter_json.rooms;
}
if (room_filter_json.rooms) {
room_filter_fields.not_rooms = room_filter_json.not_rooms;
}
this._include_leave = room_filter_json.include_leave || false;
}
this._room_filter = new FilterComponent(room_filter_fields);
this._room_timeline_filter = new FilterComponent(
room_filter_json ? (room_filter_json.timeline || {}) : {}
);
// don't bother porting this from synapse yet:
// this._room_state_filter =
// new FilterComponent(room_filter_json.state || {});
// this._room_ephemeral_filter =
// new FilterComponent(room_filter_json.ephemeral || {});
// this._room_account_data_filter =
// new FilterComponent(room_filter_json.account_data || {});
// this._presence_filter =
// new FilterComponent(definition.presence || {});
// this._account_data_filter =
// new FilterComponent(definition.account_data || {});
};
/**
* Get the room.timeline filter component of the filter
* @return {FilterComponent} room timeline filter component
*/
Filter.prototype.getRoomTimelineFilterComponent = function() {
return this._room_timeline_filter;
};
/**
* Filter the list of events based on whether they are allowed in a timeline
* based on this filter
* @param {MatrixEvent[]} events the list of events being filtered
* @return {MatrixEvent[]} the list of events which match the filter
*/
Filter.prototype.filterRoomTimeline = function(events) {
return this._room_timeline_filter.filter(this._room_filter.filter(events));
};
/**
+307 -112
View File
@@ -63,9 +63,11 @@ module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
* requests. This function must look like function(opts, callback){ ... }.
* @param {string} opts.prefix Required. The matrix client prefix to use, e.g.
* '/_matrix/client/r0'. See PREFIX_R0 and PREFIX_UNSTABLE for constants.
* @param {bool} opts.onlyData True to return only the 'data' component of the
* response (e.g. the parsed HTTP body). If false, requests will return status
* codes and headers in addition to data. Default: false.
*
* @param {bool=} opts.onlyData True to return only the 'data' component of the
* response (e.g. the parsed HTTP body). If false, requests will return an
* object with the properties <tt>code</tt>, <tt>headers</tt> and <tt>data</tt>.
*
* @param {string} opts.accessToken The access_token to send with requests. Can be
* null to not send an access token.
* @param {Object} opts.extraParams Optional. Extra query parameters to send on
@@ -99,22 +101,87 @@ module.exports.MatrixHttpApi.prototype = {
/**
* Upload content to the Home Server
* @param {File} file A File object (in a browser) or in Node,
an object with properties:
name: The file's name
stream: A read stream
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
*
* @param {object} file The object to upload. On a browser, something that
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
* a Buffer, String or ReadStream.
*
* @param {object} opts options object
*
* @param {string=} opts.name Name to give the file on the server. Defaults
* to <tt>file.name</tt>.
*
* @param {string=} opts.type Content-type for the upload. Defaults to
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
*
* @param {boolean=} opts.rawResponse Return the raw body, rather than
* parsing the JSON. Defaults to false (except on node.js, where it
* defaults to true for backwards compatibility).
*
* @param {boolean=} opts.onlyContentUri Just return the content URI,
* rather than the whole body. Defaults to false (except on browsers,
* where it defaults to true for backwards compatibility). Ignored if
* opts.rawResponse is true.
*
* @param {Function=} opts.callback Deprecated. Optional. The callback to
* invoke on success/failure. See the promise return values for more
* information.
*
* @return {module:client.Promise} Resolves to response object, as
* determined by this.opts.onlyData, opts.rawResponse, and
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
*/
uploadContent: function(file, callback) {
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
);
uploadContent: function(file, opts) {
if (utils.isFunction(opts)) {
// opts used to be callback
opts = {
callback: opts,
};
} else if (opts === undefined) {
opts = {};
}
var defer = q.defer();
var url = this.opts.baseUrl + "/_matrix/media/v1/upload";
// if the file doesn't have a mime type, use a default since
// the HS errors if we don't supply one.
var contentType = opts.type || file.type || 'application/octet-stream';
var fileName = opts.name || file.name;
// we used to recommend setting file.stream to the thing to upload on
// nodejs.
var body = file.stream ? file.stream : file;
// backwards-compatibility hacks where we used to do different things
// between browser and node.
var rawResponse = opts.rawResponse;
if (rawResponse === undefined) {
if (global.XMLHttpRequest) {
rawResponse = false;
} else {
console.warn(
"Returning the raw JSON from uploadContent(). Future " +
"versions of the js-sdk will change this default, to " +
"return the parsed object. Set opts.rawResponse=false " +
"to change this behaviour now."
);
rawResponse = true;
}
}
var onlyContentUri = opts.onlyContentUri;
if (!rawResponse && onlyContentUri === undefined) {
if (global.XMLHttpRequest) {
console.warn(
"Returning only the content-uri from uploadContent(). " +
"Future versions of the js-sdk will change this " +
"default, to return the whole response object. Set " +
"opts.onlyContentUri=false to change this behaviour now."
);
onlyContentUri = true;
} else {
onlyContentUri = false;
}
}
// browser-request doesn't support File objects because it deep-copies
// the options using JSON.parse(JSON.stringify(options)). Instead of
// loading the whole file into memory as a string and letting
@@ -124,40 +191,60 @@ module.exports.MatrixHttpApi.prototype = {
// of important here)
var upload = { loaded: 0, total: 0 };
var promise;
// XMLHttpRequest doesn't parse JSON for us. request normally does, but
// we're setting opts.json=false so that it doesn't JSON-encode the
// request, which also means it doesn't JSON-decode the response. Either
// way, we have to JSON-parse the response ourselves.
var bodyParser = null;
if (!rawResponse) {
bodyParser = function(rawBody) {
var body = JSON.parse(rawBody);
if (onlyContentUri) {
body = body.content_uri;
if (body === undefined) {
throw Error('Bad response');
}
}
return body;
};
}
if (global.XMLHttpRequest) {
var defer = q.defer();
var xhr = new global.XMLHttpRequest();
upload.xhr = xhr;
var cb = requestCallback(defer, callback, this.opts.onlyData);
var cb = requestCallback(defer, opts.callback, this.opts.onlyData);
var timeout_fn = function() {
xhr.abort();
cb(new Error('Timeout'));
};
// set an initial timeout of 30s; we'll advance it each time we get
// a progress notification
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
xhr.onreadystatechange = function() {
switch (xhr.readyState) {
case global.XMLHttpRequest.DONE:
callbacks.clearTimeout(xhr.timeout_timer);
var err;
if (!xhr.responseText) {
err = new Error('No response body.');
var resp;
try {
if (!xhr.responseText) {
throw new Error('No response body.');
}
resp = xhr.responseText;
if (bodyParser) {
resp = bodyParser(resp);
}
} catch (err) {
err.http_status = xhr.status;
cb(err);
return;
}
var resp = JSON.parse(xhr.responseText);
if (resp.content_uri === undefined) {
err = Error('Bad response');
err.http_status = xhr.status;
cb(err);
return;
}
cb(undefined, xhr, resp.content_uri);
cb(undefined, xhr, resp);
break;
}
};
@@ -168,58 +255,57 @@ module.exports.MatrixHttpApi.prototype = {
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
defer.notify(ev);
});
var url = this.opts.baseUrl + "/_matrix/media/v1/upload";
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
url += "&filename=" + encodeURIComponent(file.name);
url += "&filename=" + encodeURIComponent(fileName);
xhr.open("POST", url);
if (file.type) {
xhr.setRequestHeader("Content-Type", file.type);
} else {
// if the file doesn't have a mime type, use a default since
// the HS errors if we don't supply one.
xhr.setRequestHeader("Content-Type", 'application/octet-stream');
}
xhr.send(file);
xhr.setRequestHeader("Content-Type", contentType);
xhr.send(body);
promise = defer.promise;
// dirty hack (as per _request) to allow the upload to be cancelled.
promise.abort = xhr.abort.bind(xhr);
} else {
var queryParams = {
filename: file.name,
access_token: this.opts.accessToken
filename: fileName,
};
upload.request = this.opts.request({
uri: url,
qs: queryParams,
method: "POST"
}, requestCallback(defer, callback, this.opts.onlyData));
file.stream.pipe(this.opts.request);
promise = this.authedRequest(
opts.callback, "POST", "/upload", queryParams, body, {
prefix: "/_matrix/media/v1",
headers: {"Content-Type": contentType},
json: false,
bodyParser: bodyParser,
}
);
}
this.uploads.push(upload);
var self = this;
upload.promise = defer.promise.finally(function() {
var uploadsKeys = Object.keys(self.uploads);
for (var i = 0; i < uploadsKeys.length; ++i) {
if (self.uploads[uploadsKeys[i]].promise === defer.promise) {
self.uploads.splice(uploadsKeys[i], 1);
// remove the upload from the list on completion
var promise0 = promise.finally(function() {
for (var i = 0; i < self.uploads.length; ++i) {
if (self.uploads[i] === upload) {
self.uploads.splice(i, 1);
return;
}
}
});
return upload.promise;
// copy our dirty abort() method to the new promise
promise0.abort = promise.abort;
upload.promise = promise0;
this.uploads.push(upload);
return promise0;
},
cancelUpload: function(promise) {
var uploadsKeys = Object.keys(this.uploads);
for (var i = 0; i < uploadsKeys.length; ++i) {
var upload = this.uploads[uploadsKeys[i]];
if (upload.promise === promise) {
if (upload.xhr !== undefined) {
upload.xhr.abort();
return true;
} else if (upload.request !== undefined) {
upload.request.abort();
return true;
}
}
if (promise.abort) {
promise.abort();
return true;
}
return false;
},
@@ -270,11 +356,22 @@ module.exports.MatrixHttpApi.prototype = {
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
*
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
*
* @param {Object=} opts additional options
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
@@ -282,18 +379,25 @@ module.exports.MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
authedRequest: function(callback, method, path, queryParams, data, localTimeoutMs) {
if (!queryParams) { queryParams = {}; }
queryParams.access_token = this.opts.accessToken;
var self = this;
authedRequest: function(callback, method, path, queryParams, data, opts) {
if (!queryParams) {
queryParams = {};
}
if (!queryParams.access_token) {
queryParams.access_token = this.opts.accessToken;
}
var request_promise = this.request(
callback, method, path, queryParams, data, localTimeoutMs
callback, method, path, queryParams, data, opts
);
var self = this;
request_promise.catch(function(err) {
if (err.errcode == 'M_UNKNOWN_TOKEN') {
self.event_emitter.emit("Session.logged_out");
}
});
// return the original promise, otherwise tests break due to it having to
// go around the event loop one more time to process the result of the request
return request_promise;
@@ -306,11 +410,22 @@ module.exports.MatrixHttpApi.prototype = {
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
*
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
*
* @param {Object=} opts additional options
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
@@ -318,9 +433,13 @@ module.exports.MatrixHttpApi.prototype = {
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
request: function(callback, method, path, queryParams, data, localTimeoutMs) {
return this.requestWithPrefix(
callback, method, path, queryParams, data, this.opts.prefix, localTimeoutMs
request: function(callback, method, path, queryParams, data, opts) {
opts = opts || {};
var prefix = opts.prefix !== undefined ? opts.prefix : this.opts.prefix;
var fullUri = this.opts.baseUrl + prefix + path;
return this.requestOtherUrl(
callback, method, fullUri, queryParams, data, opts
);
},
@@ -346,16 +465,16 @@ module.exports.MatrixHttpApi.prototype = {
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*
* @deprecated prefer authedRequest with opts.prefix
*/
authedRequestWithPrefix: function(callback, method, path, queryParams, data,
prefix, localTimeoutMs) {
var fullUri = this.opts.baseUrl + prefix + path;
if (!queryParams) {
queryParams = {};
}
queryParams.access_token = this.opts.accessToken;
return this._request(
callback, method, fullUri, queryParams, data, localTimeoutMs
return this.authedRequest(
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
}
);
},
@@ -381,15 +500,16 @@ module.exports.MatrixHttpApi.prototype = {
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*
* @deprecated prefer request with opts.prefix
*/
requestWithPrefix: function(callback, method, path, queryParams, data, prefix,
localTimeoutMs) {
var fullUri = this.opts.baseUrl + prefix + path;
if (!queryParams) {
queryParams = {};
}
return this._request(
callback, method, fullUri, queryParams, data, localTimeoutMs
return this.request(
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
}
);
},
@@ -399,11 +519,22 @@ module.exports.MatrixHttpApi.prototype = {
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} uri The HTTP URI
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
*
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
*
* @param {Object=} opts additional options
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
@@ -412,12 +543,18 @@ module.exports.MatrixHttpApi.prototype = {
* occurred. This includes network problems and Matrix-specific error JSON.
*/
requestOtherUrl: function(callback, method, uri, queryParams, data,
localTimeoutMs) {
if (!queryParams) {
queryParams = {};
opts) {
if (opts === undefined || opts === null) {
opts = {};
} else if (isFinite(opts)) {
// opts used to be localTimeoutMs
opts = {
localTimeoutMs: opts
};
}
return this._request(
callback, method, uri, queryParams, data, localTimeoutMs
callback, method, uri, queryParams, data, opts
);
},
@@ -440,27 +577,56 @@ module.exports.MatrixHttpApi.prototype = {
return this.opts.baseUrl + prefix + path + queryString;
},
_request: function(callback, method, uri, queryParams, data, localTimeoutMs) {
/**
* @private
*
* @param {function} callback
* @param {string} method
* @param {string} uri
* @param {object} queryParams
* @param {object|string} data
* @param {object=} opts
*
* @param {boolean} [opts.json =true] Json-encode data before sending, and
* decode response on receipt. (We will still json-decode error
* responses, even if this is false.)
*
* @param {object=} opts.headers extra request headers
*
* @param {number=} opts.localTimeoutMs client-side timeout for the
* request. No timeout if undefined.
*
* @param {function=} opts.bodyParser function to parse the body of the
* response before passing it to the promise and callback.
*
* @return {module:client.Promise} a promise which resolves to either the
* response object (if this.opts.onlyData is truthy), or the parsed
* body. Rejects
*/
_request: function(callback, method, uri, queryParams, data, opts) {
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
);
}
opts = opts || {};
var self = this;
if (!queryParams) {
queryParams = {};
}
if (this.opts.extraParams) {
for (var key in this.opts.extraParams) {
if (!this.opts.extraParams.hasOwnProperty(key)) { continue; }
queryParams[key] = this.opts.extraParams[key];
}
}
var json = opts.json === undefined ? true : opts.json;
var defer = q.defer();
var timeoutId;
var timedOut = false;
var req;
var localTimeoutMs = opts.localTimeoutMs;
if (localTimeoutMs) {
timeoutId = callbacks.setTimeout(function() {
timedOut = true;
@@ -485,8 +651,9 @@ module.exports.MatrixHttpApi.prototype = {
withCredentials: false,
qs: queryParams,
body: data,
json: true,
json: json,
timeout: localTimeoutMs,
headers: opts.headers || {},
_matrix_opts: this.opts
},
function(err, response, body) {
@@ -496,7 +663,15 @@ module.exports.MatrixHttpApi.prototype = {
return; // already rejected promise
}
}
var handlerFn = requestCallback(defer, callback, self.opts.onlyData);
// if json is falsy, we won't parse any error response, so need
// to do so before turning it into a MatrixError
var parseErrorJson = !json;
var handlerFn = requestCallback(
defer, callback, self.opts.onlyData,
parseErrorJson,
opts.bodyParser
);
handlerFn(err, response, body);
}
);
@@ -523,14 +698,34 @@ module.exports.MatrixHttpApi.prototype = {
*
* If onlyData is true, the defer/callback is invoked with the body of the
* response, otherwise the result code.
*
* If parseErrorJson is true, we will JSON.parse the body if we get a 4xx error.
*
*/
var requestCallback = function(defer, userDefinedCallback, onlyData) {
var requestCallback = function(
defer, userDefinedCallback, onlyData,
parseErrorJson, bodyParser
) {
userDefinedCallback = userDefinedCallback || function() {};
return function(err, response, body) {
if (!err && response.statusCode >= 400) {
err = new module.exports.MatrixError(body);
err.httpStatus = response.statusCode;
if (!err) {
try {
if (response.statusCode >= 400) {
if (parseErrorJson) {
// we won't have json-decoded the response.
body = JSON.parse(body);
}
err = new module.exports.MatrixError(body);
} else if (bodyParser) {
body = bodyParser(body);
}
} catch (e) {
err = e;
}
if (err) {
err.httpStatus = response.statusCode;
}
}
if (err) {
+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;
+38 -2
View File
@@ -30,11 +30,13 @@ module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi;
module.exports.MatrixError = require("./http-api").MatrixError;
/** The {@link module:client.MatrixClient|MatrixClient} class. */
module.exports.MatrixClient = require("./client").MatrixClient;
/** The {@link module:models/room~Room|Room} class. */
/** The {@link module:models/room|Room} class. */
module.exports.Room = require("./models/room");
/** The {@link module:models/event-timeline~EventTimeline} class. */
module.exports.EventTimeline = require("./models/event-timeline");
/** The {@link module:models/room-member~RoomMember|RoomMember} class. */
/** The {@link module:models/event-timeline-set~EventTimelineSet} class. */
module.exports.EventTimelineSet = require("./models/event-timeline-set");
/** The {@link module:models/room-member|RoomMember} class. */
module.exports.RoomMember = require("./models/room-member");
/** The {@link module:models/room-state~RoomState|RoomState} class. */
module.exports.RoomState = require("./models/room-state");
@@ -53,6 +55,9 @@ module.exports.ContentRepo = require("./content-repo");
module.exports.Filter = require("./filter");
/** The {@link module:timeline-window~TimelineWindow} class. */
module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
/** The {@link module:interactive-auth} class. */
module.exports.InteractiveAuth = require("./interactive-auth");
/**
* Create a new Matrix Call.
@@ -77,6 +82,27 @@ module.exports.request = function(r) {
request = r;
};
/**
* Return the currently-set request function.
* @return {requestFunction} The current request function.
*/
module.exports.getRequest = function() {
return request;
};
/**
* Apply wrapping code around the request function. The wrapper function is
* installed as the new request handler, and when invoked it is passed the
* previous value, along with the options and callback arguments.
* @param {requestWrapperFunction} wrapper The wrapping function.
*/
module.exports.wrapRequest = function(wrapper) {
var origRequest = request;
request = function(options, callback) {
return wrapper(origRequest, options, callback);
};
};
/**
* Construct a Matrix Client. Similar to {@link module:client~MatrixClient}
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
@@ -124,6 +150,16 @@ module.exports.createClient = function(opts) {
* @param {requestCallback} callback The request callback.
*/
/**
* A wrapper for the request function interface.
* @callback requestWrapperFunction
* @param {requestFunction} origRequest The underlying request function being
* wrapped
* @param {Object} opts The options for this HTTP request, given in the same
* form as {@link requestFunction}.
* @param {requestCallback} callback The request callback.
*/
/**
* The request callback interface for performing HTTP requests. This matches the
* API for the {@link https://github.com/request/request#requestoptions-callback|
+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
*/
+54 -24
View File
@@ -25,16 +25,17 @@ var MatrixEvent = require("./event").MatrixEvent;
* <p>Once a timeline joins up with its neighbour, they are linked together into a
* doubly-linked list.
*
* @param {string} roomId the ID of the room where this timeline came from
* @param {EventTimelineSet} eventTimelineSet the set of timelines this is part of
* @constructor
*/
function EventTimeline(roomId) {
this._roomId = roomId;
function EventTimeline(eventTimelineSet) {
this._eventTimelineSet = eventTimelineSet;
this._roomId = eventTimelineSet.room ? eventTimelineSet.room.roomId : null;
this._events = [];
this._baseIndex = 0;
this._startState = new RoomState(roomId);
this._startState = new RoomState(this._roomId);
this._startState.paginationToken = null;
this._endState = new RoomState(roomId);
this._endState = new RoomState(this._roomId);
this._endState.paginationToken = null;
this._prevTimeline = null;
@@ -43,7 +44,7 @@ function EventTimeline(roomId) {
// this is used by client.js
this._paginationRequests = {'b': null, 'f': null};
this._name = roomId + ":" + new Date().toISOString();
this._name = this._roomId + ":" + new Date().toISOString();
}
/**
@@ -91,6 +92,22 @@ EventTimeline.prototype.getRoomId = function() {
return this._roomId;
};
/**
* Get the filter for this timeline's timelineSet (if any)
* @return {Filter} filter
*/
EventTimeline.prototype.getFilter = function() {
return this._eventTimelineSet.getFilter();
};
/**
* Get the timelineSet for this timeline
* @return {EventTimelineSet} timelineSet
*/
EventTimeline.prototype.getTimelineSet = function() {
return this._eventTimelineSet;
};
/**
* Get the base index.
*
@@ -217,23 +234,29 @@ EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction)
EventTimeline.prototype.addEvent = function(event, atStart) {
var stateContext = atStart ? this._startState : this._endState;
setEventMetadata(event, stateContext, atStart);
// only call setEventMetadata on the unfiltered timelineSets
var timelineSet = this.getTimelineSet();
if (timelineSet.room &&
timelineSet.room.getUnfilteredTimelineSet() === timelineSet)
{
EventTimeline.setEventMetadata(event, stateContext, atStart);
// modify state
if (event.isState()) {
stateContext.setStateEvents([event]);
// it is possible that the act of setting the state event means we
// can set more metadata (specifically sender/target props), so try
// it again if the prop wasn't previously set. It may also mean that
// the sender/target is updated (if the event set was a room member event)
// so we want to use the *updated* member (new avatar/name) instead.
//
// However, we do NOT want to do this on member events if we're going
// back in time, else we'll set the .sender value for BEFORE the given
// member event, whereas we want to set the .sender value for the ACTUAL
// member event itself.
if (!event.sender || (event.getType() === "m.room.member" && !atStart)) {
setEventMetadata(event, stateContext, atStart);
// modify state
if (event.isState()) {
stateContext.setStateEvents([event]);
// it is possible that the act of setting the state event means we
// can set more metadata (specifically sender/target props), so try
// it again if the prop wasn't previously set. It may also mean that
// the sender/target is updated (if the event set was a room member event)
// so we want to use the *updated* member (new avatar/name) instead.
//
// However, we do NOT want to do this on member events if we're going
// back in time, else we'll set the .sender value for BEFORE the given
// member event, whereas we want to set the .sender value for the ACTUAL
// member event itself.
if (!event.sender || (event.getType() === "m.room.member" && !atStart)) {
EventTimeline.setEventMetadata(event, stateContext, atStart);
}
}
}
@@ -251,7 +274,14 @@ EventTimeline.prototype.addEvent = function(event, atStart) {
}
};
function setEventMetadata(event, stateContext, toStartOfTimeline) {
/**
* Static helper method to set sender and target properties
*
* @param {MatrixEvent} event the event whose metadata is to be set
* @param {RoomState} stateContext the room state to be queried
* @param {bool} toStartOfTimeline if true the event's forwardLooking flag is set false
*/
EventTimeline.setEventMetadata = function(event, stateContext, toStartOfTimeline) {
// set sender and target properties
event.sender = stateContext.getSentinelMember(
event.getSender()
@@ -270,7 +300,7 @@ function setEventMetadata(event, stateContext, toStartOfTimeline) {
event.forwardLooking = false;
}
}
}
};
/**
* Remove an event from the timeline
+174 -23
View File
@@ -21,6 +21,10 @@ limitations under the License.
* @module models/event
*/
var EventEmitter = require("events").EventEmitter;
var utils = require('../utils.js');
/**
* Enum for event statuses.
* @readonly
@@ -29,6 +33,10 @@ limitations under the License.
module.exports.EventStatus = {
/** The event was not sent and will no longer be retried. */
NOT_SENT: "not_sent",
/** The message is being encrypted */
ENCRYPTING: "encrypting",
/** The event is in the process of being sent. */
SENDING: "sending",
/** The event is in a queue waiting to be sent. */
@@ -44,12 +52,14 @@ module.exports.EventStatus = {
/**
* Construct a Matrix Event object
* @constructor
*
* @param {Object} event The raw event to be wrapped in this DAO
* @param {boolean} encrypted Was the event encrypted
* @prop {Object} event The raw event. <b>Do not access this property</b>
* directly unless you absolutely have to. Prefer the getter methods defined on
* this class. Using the getter methods shields your app from
* changes to event JSON between Matrix versions.
*
* @prop {Object} event The raw (possibly encrypted) event. <b>Do not access
* this property</b> directly unless you absolutely have to. Prefer the getter
* methods defined on this class. Using the getter methods shields your app
* from changes to event JSON between Matrix versions.
*
* @prop {RoomMember} sender The room member who sent this event, or null e.g.
* this is a presence event.
* @prop {RoomMember} target The room member who is the target of this event, e.g.
@@ -59,15 +69,24 @@ module.exports.EventStatus = {
* that getDirectionalContent() will return event.content and not event.prev_content.
* Default: true. <strong>This property is experimental and may change.</strong>
*/
module.exports.MatrixEvent = function MatrixEvent(event, encrypted) {
module.exports.MatrixEvent = function MatrixEvent(
event
) {
this.event = event || {};
this.sender = null;
this.target = null;
this.status = null;
this.forwardLooking = true;
this.encrypted = Boolean(encrypted);
this._pushActions = null;
this._clearEvent = {};
this._keysProved = {};
this._keysClaimed = {};
};
module.exports.MatrixEvent.prototype = {
utils.inherits(module.exports.MatrixEvent, EventEmitter);
utils.extend(module.exports.MatrixEvent.prototype, {
/**
* Get the event_id for this event.
@@ -87,19 +106,22 @@ module.exports.MatrixEvent.prototype = {
},
/**
* Get the type of event.
* Get the (decrypted, if necessary) type of event.
*
* @return {string} The event type, e.g. <code>m.room.message</code>
*/
getType: function() {
return this.event.type;
return this._clearEvent.type || this.event.type;
},
/**
* Get the type of the event that will be sent to the homeserver.
* Get the (possibly encrypted) type of the event that will be sent to the
* homeserver.
*
* @return {string} The event type.
*/
getWireType: function() {
return this.encryptedType || this.event.type;
return this.event.type;
},
/**
@@ -121,19 +143,22 @@ module.exports.MatrixEvent.prototype = {
},
/**
* Get the event content JSON.
* Get the (decrypted, if necessary) event content JSON.
*
* @return {Object} The event content JSON, or an empty object.
*/
getContent: function() {
return this.event.content || {};
return this._clearEvent.content || this.event.content || {};
},
/**
* Get the event content JSON that will be sent to the homeserver.
* Get the (possibly encrypted) event content JSON that will be sent to the
* homeserver.
*
* @return {Object} The event content JSON, or an empty object.
*/
getWireContent: function() {
return this.encryptedContent || this.event.content || {};
return this.event.content || {};
},
/**
@@ -186,12 +211,95 @@ module.exports.MatrixEvent.prototype = {
return this.event.state_key !== undefined;
},
/**
* Replace the content of this event with encrypted versions.
* (This is used when sending an event; it should not be used by applications).
*
* @internal
*
* @param {string} crypto_type type of the encrypted event - typically
* <tt>"m.room.encrypted"</tt>
*
* @param {object} crypto_content raw 'content' for the encrypted event.
* @param {object} keys The local keys claimed and proved by this event.
*/
makeEncrypted: function(crypto_type, crypto_content, keys) {
// keep the plain-text data for 'view source'
this._clearEvent = {
type: this.event.type,
content: this.event.content,
};
this.event.type = crypto_type;
this.event.content = crypto_content;
this._keysProved = keys;
this._keysClaimed = keys;
},
/**
* Update the cleartext data on this event.
*
* (This is used after decrypting an event; it should not be used by applications).
*
* @internal
*
* @fires module:models/event.MatrixEvent#"Event.decrypted"
*
* @param {Object} clearEvent The plaintext payload for the event
* (typically containing <tt>type</tt> and <tt>content</tt> fields).
*
* @param {Object=} keysProved Keys owned by the sender of this event.
* See {@link module:models/event.MatrixEvent#getKeysProved}.
*
* @param {Object=} keysClaimed Keys the sender of this event claims.
* See {@link module:models/event.MatrixEvent#getKeysClaimed}.
*/
setClearData: function(clearEvent, keysProved, keysClaimed) {
this._clearEvent = clearEvent;
this._keysProved = keysProved || {};
this._keysClaimed = keysClaimed || {};
this.emit("Event.decrypted", this);
},
/**
* Check if the event is encrypted.
* @return {boolean} True if this event is encrypted.
*/
isEncrypted: function() {
return this.encrypted;
return this.event.type === "m.room.encrypted";
},
/**
* The curve25519 key that sent this event
* @return {string}
*/
getSenderKey: function() {
return this.getKeysProved().curve25519 || null;
},
/**
* The keys that must have been owned by the sender of this encrypted event.
* <p>
* These don't necessarily have to come from this event itself, but may be
* implied by the cryptographic session.
*
* @return {Object<string, string>}
*/
getKeysProved: function() {
return this._keysProved;
},
/**
* The additional keys the sender of this encrypted event claims to possess.
* <p>
* These don't necessarily have to come from this event itself, but may be
* implied by the cryptographic session.
* For example megolm messages don't claim keys directly, but instead
* inherit a claim from the olm message that established the session.
*
* @return {Object<string, string>}
*/
getKeysClaimed: function() {
return this._keysClaimed;
},
getUnsigned: function() {
@@ -202,13 +310,25 @@ module.exports.MatrixEvent.prototype = {
* Update the content of an event in the same way it would be by the server
* if it were redacted before it was sent to us
*
* @param {Object} the raw event causing the redaction
* @param {module:models/event.MatrixEvent} redaction_event
* event causing the redaction
*/
makeRedacted: function(redaction_event) {
// quick sanity-check
if (!redaction_event.event) {
throw new Error("invalid redaction_event in makeRedacted");
}
// we attempt to replicate what we would see from the server if
// the event had been redacted before we saw it.
//
// The server removes (most of) the content of the event, and adds a
// "redacted_because" key to the unsigned section containing the
// redacted event.
if (!this.event.unsigned) {
this.event.unsigned = {};
}
this.event.unsigned.redacted_because = redaction_event;
this.event.unsigned.redacted_because = redaction_event.event;
var key;
for (key in this.event) {
@@ -219,10 +339,11 @@ module.exports.MatrixEvent.prototype = {
}
var keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {};
for (key in this.event.content) {
if (!this.event.content.hasOwnProperty(key)) { continue; }
var content = this.getContent();
for (key in content) {
if (!content.hasOwnProperty(key)) { continue; }
if (!keeps[key]) {
delete this.event.content[key];
delete content[key];
}
}
},
@@ -235,7 +356,25 @@ module.exports.MatrixEvent.prototype = {
isRedacted: function() {
return Boolean(this.getUnsigned().redacted_because);
},
};
/**
* Get the push actions, if known, for this event
*
* @return {?Object} push actions
*/
getPushActions: function() {
return this._pushActions;
},
/**
* Set the push actions for this event.
*
* @param {Object} pushActions push actions
*/
setPushActions: function(pushActions) {
this._pushActions = pushActions;
},
});
/* http://matrix.org/docs/spec/r0.0.1/client_server.html#redactions says:
@@ -276,3 +415,15 @@ var _REDACT_KEEP_CONTENT_MAP = {
},
'm.room.aliases': {'aliases': 1},
};
/**
* Fires when an event is decrypted
*
* @event module:models/event.MatrixEvent#"Event.decrypted"
*
* @param {module:models/event.MatrixEvent} event
* The matrix event which has been decrypted
*/
+18 -23
View File
@@ -24,7 +24,10 @@ var utils = require("../utils");
/**
* Construct a new room member.
*
* @constructor
* @alias module:models/room-member
*
* @param {string} roomId The room ID of the member.
* @param {string} userId The user ID of the member.
* @prop {string} roomId The room ID for this member.
@@ -77,11 +80,11 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) {
this.name = calculateDisplayName(this, event, roomState);
if (oldMembership !== this.membership) {
this._updateModifiedTime();
this.emit("RoomMember.membership", event, this);
this.emit("RoomMember.membership", event, this, oldMembership);
}
if (oldName !== this.name) {
this._updateModifiedTime();
this.emit("RoomMember.name", event, this);
this.emit("RoomMember.name", event, this, oldName);
}
};
@@ -102,11 +105,14 @@ RoomMember.prototype.setPowerLevelEvent = function(powerLevelEvent) {
});
var oldPowerLevel = this.powerLevel;
var oldPowerLevelNorm = this.powerLevelNorm;
this.powerLevel = (
powerLevelEvent.getContent().users[this.userId] ||
powerLevelEvent.getContent().users_default ||
0
);
if (powerLevelEvent.getContent().users[this.userId] !== undefined) {
this.powerLevel = powerLevelEvent.getContent().users[this.userId];
} else if (powerLevelEvent.getContent().users_default !== undefined) {
this.powerLevel = powerLevelEvent.getContent().users_default;
} else {
this.powerLevel = 0;
}
this.powerLevelNorm = 0;
if (maxLevel > 0) {
this.powerLevelNorm = (this.powerLevel * 100) / maxLevel;
@@ -206,21 +212,6 @@ function calculateDisplayName(member, event, roomState) {
var displayName = event.getDirectionalContent().displayname;
var selfUserId = member.userId;
/*
// FIXME: this would be great but still needs to use the
// full userId to disambiguate if needed...
if (!displayName) {
var matches = selfUserId.match(/^@(.*?):/);
if (matches) {
return matches[1];
}
else {
return selfUserId;
}
}
*/
if (!displayName) {
return selfUserId;
}
@@ -249,6 +240,8 @@ module.exports = RoomMember;
* @event module:client~MatrixClient#"RoomMember.name"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomMember} member The member whose RoomMember.name changed.
* @param {string?} oldName The previous name. Null if the member didn't have a
* name previously.
* @example
* matrixClient.on("RoomMember.name", function(event, member){
* var newName = member.name;
@@ -260,8 +253,10 @@ module.exports = RoomMember;
* @event module:client~MatrixClient#"RoomMember.membership"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomMember} member The member whose RoomMember.membership changed.
* @param {string?} oldMembership The previous membership state. Null if it's a
* new member.
* @example
* matrixClient.on("RoomMember.membership", function(event, member){
* matrixClient.on("RoomMember.membership", function(event, member, oldMembership){
* var newState = member.membership;
* });
*/
+2 -1
View File
@@ -25,7 +25,8 @@ var RoomMember = require("./room-member");
/**
* Construct room state.
* @constructor
* @param {string} roomId Required. The ID of the room which has this state.
* @param {?string} roomId Optional. The ID of the room which has this state.
* If none is specified it just tracks paginationTokens, useful for notifTimelineSet
* @prop {Object.<string, RoomMember>} members The room member dictionary, keyed
* on the user's ID.
* @prop {Object.<string, Object.<string, MatrixEvent>>} events The state
+271 -497
View File
File diff suppressed because it is too large Load Diff
+60 -7
View File
@@ -30,6 +30,7 @@ limitations under the License.
* @prop {string} displayName The 'displayname' of the user if known.
* @prop {string} avatarUrl The 'avatar_url' of the user if known.
* @prop {string} presence The presence enum if known.
* @prop {string} presenceStatusMsg The presence status message if known.
* @prop {Number} lastActiveAgo The time elapsed in ms since the user interacted
* proactively with the server, or we saw a message from the user
* @prop {Number} lastPresenceTs Timestamp (ms since the epoch) for when we last
@@ -44,7 +45,9 @@ limitations under the License.
function User(userId) {
this.userId = userId;
this.presence = "offline";
this.presenceStatusMsg = null;
this.displayName = userId;
this.rawDisplayName = userId;
this.avatarUrl = null;
this.lastActiveAgo = 0;
this.lastPresenceTs = 0;
@@ -77,23 +80,39 @@ User.prototype.setPresenceEvent = function(event) {
if (event.getContent().presence !== this.presence || firstFire) {
eventsToFire.push("User.presence");
}
if (event.getContent().avatar_url !== this.avatarUrl) {
if (event.getContent().avatar_url &&
event.getContent().avatar_url !== this.avatarUrl)
{
eventsToFire.push("User.avatarUrl");
}
if (event.getContent().displayname !== this.displayName) {
if (event.getContent().displayname &&
event.getContent().displayname !== this.displayName)
{
eventsToFire.push("User.displayName");
}
if (event.getContent().currently_active !== undefined &&
event.getContent().currently_active !== this.currentlyActive)
{
eventsToFire.push("User.currentlyActive");
}
this.presence = event.getContent().presence;
this.displayName = event.getContent().displayname;
this.avatarUrl = event.getContent().avatar_url;
eventsToFire.push("User.lastPresenceTs");
if (event.getContent().status_msg) {
this.presenceStatusMsg = event.getContent().status_msg;
}
if (event.getContent().displayname) {
this.displayName = event.getContent().displayname;
}
if (event.getContent().avatar_url) {
this.avatarUrl = event.getContent().avatar_url;
}
this.lastActiveAgo = event.getContent().last_active_ago;
this.lastPresenceTs = Date.now();
this.currentlyActive = event.getContent().currently_active;
if (eventsToFire.length > 0) {
this._updateModifiedTime();
}
this._updateModifiedTime();
for (var i = 0; i < eventsToFire.length; i++) {
this.emit(eventsToFire[i], event, this);
@@ -113,6 +132,17 @@ User.prototype.setDisplayName = function(name) {
}
};
/**
* Manually set this user's non-disambiguated display name. No event is emitted
* in response to this as there is no underlying MatrixEvent to emit with.
* @param {string} name The new display name.
*/
User.prototype.setRawDisplayName = function(name) {
this.rawDisplayName = name;
};
/**
* Manually set this user's avatar URL. No event is emitted in response to this
* as there is no underlying MatrixEvent to emit with.
@@ -157,6 +187,18 @@ User.prototype.getLastActiveTs = function() {
*/
module.exports = User;
/**
* Fires whenever any user's lastPresenceTs changes,
* ie. whenever any presence event is received for a user.
* @event module:client~MatrixClient#"User.lastPresenceTs"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {User} user The user whose User.lastPresenceTs changed.
* @example
* matrixClient.on("User.lastPresenceTs", function(event, user){
* var newlastPresenceTs = user.lastPresenceTs;
* });
*/
/**
* Fires whenever any user's presence changes.
* @event module:client~MatrixClient#"User.presence"
@@ -168,6 +210,17 @@ module.exports = User;
* });
*/
/**
* Fires whenever any user's currentlyActive changes.
* @event module:client~MatrixClient#"User.currentlyActive"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {User} user The user whose User.currentlyActive changed.
* @example
* matrixClient.on("User.currentlyActive", function(event, user){
* var newCurrentlyActive = user.currentlyActive;
* });
*/
/**
* Fires whenever any user's display name changes.
* @event module:client~MatrixClient#"User.displayName"
+52 -21
View File
@@ -122,7 +122,7 @@ function PushProcessor(client) {
var eventFulfillsRoomMemberCountCondition = function(cond, ev) {
if (!cond.is) { return false; }
var room = client.getRoom(ev.room_id);
var room = client.getRoom(ev.getRoomId());
if (!room || !room.currentState || !room.currentState.members) { return false; }
var memberCount = Object.keys(room.currentState.members).filter(function(m) {
@@ -152,11 +152,12 @@ function PushProcessor(client) {
};
var eventFulfillsDisplayNameCondition = function(cond, ev) {
if (!ev.content || ! ev.content.body || typeof ev.content.body != 'string') {
var content = ev.getContent();
if (!content || !content.body || typeof content.body != 'string') {
return false;
}
var room = client.getRoom(ev.room_id);
var room = client.getRoom(ev.getRoomId());
if (!room || !room.currentState || !room.currentState.members ||
!room.currentState.getMember(client.credentials.userId)) { return false; }
@@ -165,7 +166,7 @@ function PushProcessor(client) {
// N.B. we can't use \b as it chokes on unicode. however \W seems to be okay
// as shorthand for [^0-9A-Za-z_].
var pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i');
return ev.content.body.search(pat) > -1;
return content.body.search(pat) > -1;
};
var eventFulfillsDeviceCondition = function(cond, ev) {
@@ -204,7 +205,21 @@ function PushProcessor(client) {
var valueForDottedKey = function(key, ev) {
var parts = key.split('.');
var val = ev;
var val;
// special-case the first component to deal with encrypted messages
var firstPart = parts[0];
if (firstPart == 'content') {
val = ev.getContent();
parts.shift();
} else if (firstPart == 'type') {
val = ev.getType();
parts.shift();
} else {
// use the raw event for any other fields
val = ev.event;
}
while (parts.length > 0) {
var thispart = parts.shift();
if (!val[thispart]) { return null; }
@@ -215,7 +230,7 @@ function PushProcessor(client) {
var matchingRuleForEventWithRulesets = function(ev, rulesets) {
if (!rulesets || !rulesets.device) { return null; }
if (ev.user_id == client.credentials.userId) { return null; }
if (ev.getSender() == client.credentials.userId) { return null; }
var allDevNames = Object.keys(rulesets.device);
for (var i = 0; i < allDevNames.length; ++i) {
@@ -228,25 +243,11 @@ function PushProcessor(client) {
return matchingRuleFromKindSet(ev, rulesets.global);
};
var actionListToActionsObject = function(actionlist) {
var actionobj = { 'notify': false, 'tweaks': {} };
for (var i = 0; i < actionlist.length; ++i) {
var action = actionlist[i];
if (action === 'notify') {
actionobj.notify = true;
} else if (typeof action === 'object') {
if (action.value === undefined) { action.value = true; }
actionobj.tweaks[action.set_tweak] = action.value;
}
}
return actionobj;
};
var pushActionsForEventAndRulesets = function(ev, rulesets) {
var rule = matchingRuleForEventWithRulesets(ev, rulesets);
if (!rule) { return {}; }
var actionObj = actionListToActionsObject(rule.actions);
var actionObj = PushProcessor.actionListToActionsObject(rule.actions);
// Some actions are implicit in some situations: we add those here
if (actionObj.tweaks.highlight === undefined) {
@@ -258,11 +259,40 @@ function PushProcessor(client) {
return actionObj;
};
/**
* Get the user's push actions for the given event
*
* @param {module:models/event.MatrixEvent} ev
*
* @return {PushAction}
*/
this.actionsForEvent = function(ev) {
return pushActionsForEventAndRulesets(ev, client.pushRules);
};
}
/**
* Convert a list of actions into a object with the actions as keys and their values
* eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ]
* becomes { notify: true, tweaks: { sound: 'default' } }
* @param {array} actionlist The actions list
*
* @return {object} A object with key 'notify' (true or false) and an object of actions
*/
PushProcessor.actionListToActionsObject = function(actionlist) {
var actionobj = { 'notify': false, 'tweaks': {} };
for (var i = 0; i < actionlist.length; ++i) {
var action = actionlist[i];
if (action === 'notify') {
actionobj.notify = true;
} else if (typeof action === 'object') {
if (action.value === undefined) { action.value = true; }
actionobj.tweaks[action.set_tweak] = action.value;
}
}
return actionobj;
};
/**
* @typedef {Object} PushAction
* @type {Object}
@@ -276,3 +306,4 @@ function PushProcessor(client) {
/** The PushProcessor class. */
module.exports = PushProcessor;
+31 -1
View File
@@ -43,6 +43,9 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
// filterId: Filter
// }
};
this.accountData = {
// type : content
};
this.localStorage = opts.localStorage;
};
@@ -98,6 +101,11 @@ module.exports.MatrixInMemoryStore.prototype = {
var user = this.users[member.userId] || new User(member.userId);
if (member.name) {
user.setDisplayName(member.name);
if (member.events.member) {
user.setRawDisplayName(
member.events.member.getDirectionalContent().displayname
);
}
}
if (member.events.member && member.events.member.getContent().avatar_url) {
user.setAvatarUrl(member.events.member.getContent().avatar_url);
@@ -244,7 +252,29 @@ module.exports.MatrixInMemoryStore.prototype = {
this.localStorage.setItem("mxjssdk_memory_filter_" + filterName, filterId);
}
catch (e) {}
}
},
/**
* Store user-scoped account data events.
* N.B. that account data only allows a single event per type, so multiple
* events with the same type will replace each other.
* @param {Array<MatrixEvent>} events The events to store.
*/
storeAccountDataEvents: function(events) {
var self = this;
events.forEach(function(event) {
self.accountData[event.getType()] = event;
});
},
/**
* Get account data event by event type
* @param {string} eventType The event type being queried
* @return {?MatrixEvent} the user account_data event of given type, if any
*/
getAccountData: function(eventType) {
return this.accountData[eventType];
},
// TODO
//setMaxHistoryPerRoom: function(maxHistory) {},
+31
View File
@@ -62,6 +62,22 @@ WebStorageSessionStore.prototype = {
return this.store.getItem(KEY_END_TO_END_ACCOUNT);
},
/**
* Store a flag indicating that we have announced the new device.
*/
setDeviceAnnounced: function() {
this.store.setItem(KEY_END_TO_END_ANNOUNCED, "true");
},
/**
* Check if the "device announced" flag is set
*
* @return {boolean} true if the "device announced" flag has been set.
*/
getDeviceAnnounced: function() {
return this.store.getItem(KEY_END_TO_END_ANNOUNCED) == "true";
},
/**
* Stores the known devices for a user.
* @param {string} userId The user's ID.
@@ -104,6 +120,16 @@ WebStorageSessionStore.prototype = {
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
},
getEndToEndInboundGroupSession: function(senderKey, sessionId) {
var key = keyEndToEndInboundGroupSession(senderKey, sessionId);
return this.store.getItem(key);
},
storeEndToEndInboundGroupSession: function(senderKey, sessionId, pickledSession) {
var key = keyEndToEndInboundGroupSession(senderKey, sessionId);
return this.store.setItem(key, pickledSession);
},
/**
* Store the end-to-end state for a room.
* @param {string} roomId The room's ID.
@@ -124,6 +150,7 @@ WebStorageSessionStore.prototype = {
};
var KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
var KEY_END_TO_END_ANNOUNCED = E2E_PREFIX + "announced";
function keyEndToEndDevicesForUser(userId) {
return E2E_PREFIX + "devices/" + userId;
@@ -133,6 +160,10 @@ function keyEndToEndSessions(deviceKey) {
return E2E_PREFIX + "sessions/" + deviceKey;
}
function keyEndToEndInboundGroupSession(senderKey, sessionId) {
return E2E_PREFIX + "inboundgroupsessions/" + senderKey + "/" + sessionId;
}
function keyEndToEndRoom(roomId) {
return E2E_PREFIX + "rooms/" + roomId;
}
+17 -1
View File
@@ -162,7 +162,23 @@ StubStore.prototype = {
*/
setFilterIdByName: function(filterName, filterId) {
}
},
/**
* Store user-scoped account data events
* @param {Array<MatrixEvent>} events The events to store.
*/
storeAccountDataEvents: function(events) {
},
/**
* Get account data event by event type
* @param {string} eventType The event type being queried
*/
getAccountData: function(eventType) {
},
// TODO
//setMaxHistoryPerRoom: function(maxHistory) {},
+170 -98
View File
@@ -60,18 +60,25 @@ function debuglog() {
function SyncApi(client, opts) {
this.client = client;
opts = opts || {};
opts.initialSyncLimit = opts.initialSyncLimit || 8;
opts.initialSyncLimit = (
opts.initialSyncLimit === undefined ? 8 : opts.initialSyncLimit
);
opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false;
opts.pollTimeout = opts.pollTimeout || (30 * 1000);
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
this.opts = opts;
this._peekRoomId = null;
this._syncConnectionLost = false;
this._currentSyncRequest = null;
this._syncState = null;
this._running = false;
this._keepAliveTimer = null;
this._connectionReturnedDefer = null;
this._notifEvents = []; // accumulator of sync events in the current sync response
if (client.getNotifTimelineSet()) {
reEmit(client, client.getNotifTimelineSet(),
["Room.timeline", "Room.timelineReset"]);
}
}
/**
@@ -88,6 +95,7 @@ SyncApi.prototype.createRoom = function(roomId) {
"Room.receipt", "Room.tags",
"Room.timelineReset",
"Room.localEchoUpdated",
"Room.accountData",
]);
this._registerStateListeners(room);
return room;
@@ -147,7 +155,7 @@ SyncApi.prototype.syncLeftRooms = function() {
timeout: 0 // don't want to block since this is a single isolated req
};
return this._getOrCreateFilter(
return client.getOrCreateFilter(
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter
).then(function(filterId) {
qps.filter = filterId;
@@ -385,12 +393,23 @@ SyncApi.prototype.sync = function() {
}
function getFilter() {
var filter = new Filter(client.credentials.userId);
filter.setTimelineLimit(self.opts.initialSyncLimit);
var filter;
if (self.opts.filter) {
filter = self.opts.filter;
} else {
filter = new Filter(client.credentials.userId);
filter.setTimelineLimit(self.opts.initialSyncLimit);
}
self._getOrCreateFilter(
client.getOrCreateFilter(
getFilterName(client.credentials.userId), filter
).done(function(filterId) {
// reset the notifications timeline to prepare it to paginate from
// the current point in time.
// The right solution would be to tie /sync pagination tokens into
// /notifications API somehow.
client.resetNotifTimelineSet();
self._sync({ filterId: filterId });
}, function(err) {
self._startKeepAlives().done(function() {
@@ -454,6 +473,7 @@ SyncApi.prototype._sync = function(syncOptions) {
self._connectionReturnedDefer = null;
}
this._updateSyncState("STOPPED");
return;
}
var filterId = syncOptions.filterId;
@@ -477,7 +497,7 @@ SyncApi.prototype._sync = function(syncOptions) {
qps._cacheBuster = Date.now();
}
if (self._syncConnectionLost) {
if (this.getSyncState() == 'ERROR' || this.getSyncState() == 'RECONNECTING') {
// we think the connection is dead. If it comes back up, we won't know
// about it till /sync returns. If the timeout= is high, this could
// be a long time. Set it to 0 when doing retries so we don't have to wait
@@ -493,38 +513,6 @@ SyncApi.prototype._sync = function(syncOptions) {
);
this._currentSyncRequest.done(function(data) {
self._syncConnectionLost = false;
// data looks like:
// {
// next_batch: $token,
// presence: { events: [] },
// rooms: {
// invite: {
// $roomid: {
// invite_state: { events: [] }
// }
// },
// join: {
// $roomid: {
// state: { events: [] },
// timeline: { events: [], prev_batch: $token, limited: true },
// ephemeral: { events: [] },
// account_data: { events: [] },
// unread_notifications: {
// highlight_count: 0,
// notification_count: 0,
// }
// }
// },
// leave: {
// $roomid: {
// state: { events: [] },
// timeline: { events: [], prev_batch: $token }
// }
// }
// }
// }
// set the sync token NOW *before* processing the events. We do this so
// if something barfs on an event we can skip it rather than constantly
// polling with the same token.
@@ -556,18 +544,25 @@ SyncApi.prototype._sync = function(syncOptions) {
self._connectionReturnedDefer.reject();
self._connectionReturnedDefer = null;
}
self._updateSyncState("STOPPED");
return;
}
console.error("/sync error %s", err);
console.error(err);
debuglog("Starting keep-alive");
self._syncConnectionLost = true;
// Note that we do *not* mark the sync connection as
// lost yet: we only do this if a keepalive poke
// fails, since long lived HTTP connections will
// go away sometimes and we shouldn't treat this as
// erroneous. We set the state to 'reconnecting'
// instead, so that clients can onserve this state
// if they wish.
self._startKeepAlives().done(function() {
self._sync(syncOptions);
});
self._currentSyncRequest = null;
self._updateSyncState("ERROR", { error: err });
self._updateSyncState("RECONNECTING");
});
};
@@ -583,6 +578,39 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
var client = this.client;
var self = this;
// data looks like:
// {
// next_batch: $token,
// presence: { events: [] },
// account_data: { events: [] },
// to_device: { events: [] },
// rooms: {
// invite: {
// $roomid: {
// invite_state: { events: [] }
// }
// },
// join: {
// $roomid: {
// state: { events: [] },
// timeline: { events: [], prev_batch: $token, limited: true },
// ephemeral: { events: [] },
// account_data: { events: [] },
// unread_notifications: {
// highlight_count: 0,
// notification_count: 0,
// }
// }
// },
// leave: {
// $roomid: {
// state: { events: [] },
// timeline: { events: [], prev_batch: $token }
// }
// }
// },
// }
// TODO-arch:
// - Each event we pass through needs to be emitted via 'event', can we
// do this in one place?
@@ -605,7 +633,44 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
});
}
// the returned json structure is abit crap, so make it into a
// handle non-room account_data
if (data.account_data && utils.isArray(data.account_data.events)) {
var events = data.account_data.events.map(client.getEventMapper());
client.store.storeAccountDataEvents(events);
events.forEach(
function(accountDataEvent) {
if (accountDataEvent.getType() == 'm.push_rules') {
client.pushRules = accountDataEvent.getContent();
}
client.emit("accountData", accountDataEvent);
return accountDataEvent;
}
);
}
// handle to-device events
if (data.to_device && utils.isArray(data.to_device.events)) {
data.to_device.events
.map(client.getEventMapper())
.forEach(
function(toDeviceEvent) {
var content = toDeviceEvent.getContent();
if (
toDeviceEvent.getType() == "m.room.message" &&
content.msgtype == "m.bad.encrypted"
) {
console.warn(
"Unable to decrypt to-device event: " + content.body
);
return;
}
client.emit("toDeviceEvent", toDeviceEvent);
}
);
}
// the returned json structure is a bit crap, so make it into a
// nicer form (array) after applying sanity to make sure we don't fail
// on missing keys (on the off chance)
var inviteRooms = [];
@@ -624,6 +689,8 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
}
}
this._notifEvents = [];
// Handle invites
inviteRooms.forEach(function(inviteObj) {
var room = inviteObj.room;
@@ -709,6 +776,12 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
room.currentState.paginationToken = syncToken;
self._deregisterStateListeners(room);
room.resetLiveTimeline(joinObj.timeline.prev_batch);
// We have to assume any gap in any timeline is
// reason to stop incrementally tracking notifications and
// reset the timeline.
client.resetNotifTimelineSet();
self._registerStateListeners(room);
}
}
@@ -757,6 +830,20 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
timelineEvents.forEach(function(e) { client.emit("event", e); });
accountDataEvents.forEach(function(e) { client.emit("event", e); });
});
// update the notification timeline, if appropriate.
// we only do this for live events, as otherwise we can't order them sanely
// in the timeline relative to ones paginated in by /notifications.
// XXX: we could fix this by making EventTimeline support chronological
// ordering... but it doesn't, right now.
if (syncToken && this._notifEvents.length) {
this._notifEvents.sort(function(a, b) {
return a.getTs() - b.getTs();
});
this._notifEvents.forEach(function(event) {
client.getNotifTimelineSet().addLiveEvent(event);
});
}
};
/**
@@ -768,17 +855,21 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
*/
SyncApi.prototype._startKeepAlives = function(delay) {
if (delay === undefined) {
delay = 5000 + Math.floor(Math.random() * 5000);
delay = 2000 + Math.floor(Math.random() * 5000);
}
if (this._keepAliveTimer !== null) {
clearTimeout(this._keepAliveTimer);
}
var self = this;
self._keepAliveTimer = setTimeout(
self._pokeKeepAlive.bind(self),
delay
);
if (delay > 0) {
self._keepAliveTimer = setTimeout(
self._pokeKeepAlive.bind(self),
delay
);
} else {
self._pokeKeepAlive();
}
if (!this._connectionReturnedDefer) {
this._connectionReturnedDefer = q.defer();
}
@@ -798,9 +889,15 @@ SyncApi.prototype._pokeKeepAlive = function() {
}
}
this.client._http.requestWithPrefix(
undefined, "GET", "/_matrix/client/versions", undefined,
undefined, "", 15 * 1000
this.client._http.request(
undefined, // callback
"GET", "/_matrix/client/versions",
undefined, // queryParams
undefined, // data
{
prefix: '',
localTimeoutMs: 15 * 1000,
}
).done(function() {
success();
}, function(err) {
@@ -816,57 +913,17 @@ SyncApi.prototype._pokeKeepAlive = function() {
self._pokeKeepAlive.bind(self),
5000 + Math.floor(Math.random() * 5000)
);
// A keepalive has failed, so we emit the
// error state (whether or not this is the
// first failure).
// Note we do this after setting the timer:
// this lets the unit tests advance the mock
// clock when the get the error.
self._updateSyncState("ERROR", { error: err });
}
});
};
/**
* @param {string} filterName
* @param {Filter} filter
* @return {Promise<String>} Filter ID
*/
SyncApi.prototype._getOrCreateFilter = function(filterName, filter) {
var client = this.client;
var filterId = client.store.getFilterIdByName(filterName);
var promise = q();
if (filterId) {
// check that the existing filter matches our expectations
promise = client.getFilter(client.credentials.userId,
filterId, true
).then(function(existingFilter) {
var oldDef = existingFilter.getDefinition();
var newDef = filter.getDefinition();
if (utils.deepCompare(oldDef, newDef)) {
// super, just use that.
debuglog("Using existing filter ID %s: %s", filterId,
JSON.stringify(oldDef));
return q(filterId);
}
debuglog("Existing filter ID %s: %s; new filter: %s",
filterId, JSON.stringify(oldDef), JSON.stringify(newDef));
return;
});
}
return promise.then(function(existingId) {
if (existingId) {
return existingId;
}
// create a new filter
return client.createFilter(filter.getDefinition()
).then(function(createdFilter) {
debuglog("Created new filter ID %s: %s", createdFilter.filterId,
JSON.stringify(createdFilter.getDefinition()));
client.store.setFilterIdByName(filterName, createdFilter.filterId);
return createdFilter.filterId;
});
});
};
/**
* @param {Object} obj
* @return {Object[]}
@@ -992,6 +1049,18 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
// may make notifications appear which should have the right name.
room.recalculate(this.client.credentials.userId);
// gather our notifications into this._notifEvents
if (client.getNotifTimelineSet()) {
for (var i = 0; i < timelineEventList.length; i++) {
var pushActions = client.getPushActionsForEvent(timelineEventList[i]);
if (pushActions && pushActions.notify &&
pushActions.tweaks && pushActions.tweaks.highlight)
{
this._notifEvents.push(timelineEventList[i]);
}
}
}
// execute the timeline events, this will begin to diverge the current state
// if the timeline has any state events in it.
room.addLiveEvents(timelineEventList);
@@ -1040,7 +1109,10 @@ SyncApi.prototype._onOnline = function() {
function createNewUser(client, userId) {
var user = new User(userId);
reEmit(client, user, ["User.avatarUrl", "User.displayName", "User.presence"]);
reEmit(client, user, [
"User.avatarUrl", "User.displayName", "User.presence",
"User.currentlyActive", "User.lastPresenceTs"
]);
return user;
}
+10 -11
View File
@@ -41,7 +41,7 @@ var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
* Construct a TimelineWindow.
*
* <p>This abstracts the separate timelines in a Matrix {@link
* module:models/room~Room|Room} into a single iterable thing. It keeps track of
* module:models/room|Room} into a single iterable thing. It keeps track of
* the start and endpoints of the window, which can be advanced with the help
* of pagination requests.
*
@@ -56,7 +56,7 @@ var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
* @param {MatrixClient} client MatrixClient to be used for context/pagination
* requests.
*
* @param {Room} room The room to track
* @param {EventTimelineSet} timelineSet The timelineSet to track
*
* @param {Object} [opts] Configuration options for this window
*
@@ -66,10 +66,10 @@ var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
*
* @constructor
*/
function TimelineWindow(client, room, opts) {
function TimelineWindow(client, timelineSet, opts) {
opts = opts || {};
this._client = client;
this._room = room;
this._timelineSet = timelineSet;
// these will be TimelineIndex objects; they delineate the 'start' and
// 'end' of the window.
@@ -113,7 +113,7 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
// TODO: ideally we'd spot getEventTimeline returning a resolved promise and
// skip straight to the find-event loop.
if (initialEventId) {
return this._client.getEventTimeline(this._room, initialEventId)
return this._client.getEventTimeline(this._timelineSet, initialEventId)
.then(function(tl) {
// make sure that our window includes the event
for (var i = 0; i < tl.getEvents().length; i++) {
@@ -126,7 +126,7 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
});
} else {
// start with the most recent events
var tl = this._room.getLiveTimeline();
var tl = this._timelineSet.getLiveTimeline();
initFields(tl, tl.getEvents().length);
return q();
}
@@ -234,7 +234,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
// remove some events from the other end, if necessary
var excess = this._eventCount - this._windowLimit;
if (excess > 0) {
this._unpaginate(excess, direction != EventTimeline.BACKWARDS);
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
}
return q(true);
}
@@ -254,6 +254,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
debuglog("TimelineWindow: starting request");
var self = this;
var prom = this._client.paginateEventTimeline(tl.timeline, {
backwards: direction == EventTimeline.BACKWARDS,
limit: size
@@ -286,15 +287,13 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
/**
* Trim the window to the windowlimit
* Remove `delta` events from the start or end of the timeline.
*
* @param {number} delta number of events to remove from the timeline
* @param {boolean} startOfTimeline if events should be removed from the start
* of the timeline.
*
* @private
*/
TimelineWindow.prototype._unpaginate = function(delta, startOfTimeline) {
TimelineWindow.prototype.unpaginate = function(delta, startOfTimeline) {
var tl = startOfTimeline ? this._start : this._end;
// sanity-check the delta
+29 -1
View File
@@ -204,7 +204,8 @@ module.exports.isFunction = function(value) {
* @return {boolean} True if it is an array.
*/
module.exports.isArray = function(value) {
return Boolean(value && value.constructor === Array);
return Array.isArray ? Array.isArray(value) :
Boolean(value && value.constructor === Array);
};
/**
@@ -332,6 +333,33 @@ var deepCompare = module.exports.deepCompare = function(x, y) {
return true;
};
/**
* Copy properties from one object to another.
*
* All enumerable properties, included inherited ones, are copied.
*
* This is approximately equivalent to ES6's Object.assign, except
* that the latter doesn't copy inherited properties.
*
* @param {Object} target The object that will receive new properties
* @param {...Object} source Objects from which to copy properties
*
* @return {Object} target
*/
module.exports.extend = function() {
var target = arguments[0] || {};
// disable jshint "The body of a for in should be wrapped in an if
// statement"
/* jshint -W089 */
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var propName in source) {
target[propName] = source[propName];
}
}
/* jshint +W089 */
return target;
};
/**
* Run polyfills to add Array.map and Array.filter if they are missing.
+128 -18
View File
@@ -60,6 +60,12 @@ function MatrixCall(opts) {
this.candidateSendQueue = [];
this.candidateSendTries = 0;
// Lookup from opaque queue ID to a promise for media element operations that
// need to be serialised into a given queue. Store this per-MatrixCall on the
// assumption that multiple matrix calls will never compete for control of the
// same DOM elements.
this.mediaPromises = Object.create(null);
this.screenSharingStream = null;
}
/** The length of time a call can be ringing for. */
@@ -144,6 +150,86 @@ MatrixCall.prototype.placeScreenSharingCall =
_tryPlayRemoteStream(this);
};
/**
* Play the given HTMLMediaElement, serialising the operation into a chain
* of promises to avoid racing access to the element
* @param {Element} HTMLMediaElement element to play
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
*/
MatrixCall.prototype.playElement = function(element, queueId) {
console.log("queuing play on " + queueId + " and element " + element);
// XXX: FIXME: Does this leak elements, given the old promises
// may hang around and retain a reference to them?
if (this.mediaPromises[queueId]) {
// XXX: these promises can fail (e.g. by <video/> being unmounted whilst
// pending receiving media to play - e.g. whilst switching between
// rooms before answering an inbound call), and throw unhandled exceptions.
// However, we should soldier on as best we can even if they fail, given
// these failures may be non-fatal (as in the case of unmounts)
this.mediaPromises[queueId] =
this.mediaPromises[queueId].then(function() {
console.log("previous promise completed for " + queueId);
return element.play();
}, function() {
console.log("previous promise failed for " + queueId);
return element.play();
});
}
else {
this.mediaPromises[queueId] = element.play();
}
};
/**
* Pause the given HTMLMediaElement, serialising the operation into a chain
* of promises to avoid racing access to the element
* @param {Element} HTMLMediaElement element to pause
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
*/
MatrixCall.prototype.pauseElement = function(element, queueId) {
console.log("queuing pause on " + queueId + " and element " + element);
if (this.mediaPromises[queueId]) {
this.mediaPromises[queueId] =
this.mediaPromises[queueId].then(function() {
console.log("previous promise completed for " + queueId);
return element.pause();
}, function() {
console.log("previous promise failed for " + queueId);
return element.pause();
});
}
else {
// pause doesn't actually return a promise, but do this for symmetry
// and just in case it does in future.
this.mediaPromises[queueId] = element.pause();
}
};
/**
* Assign the given HTMLMediaElement by setting the .src attribute on it,
* serialising the operation into a chain of promises to avoid racing access
* to the element
* @param {Element} HTMLMediaElement element to pause
* @param {string} src the src attribute value to assign to the element
* @param {string} queueId Arbitrary ID to track the chain of promises to be used
*/
MatrixCall.prototype.assignElement = function(element, src, queueId) {
console.log("queuing assign on " + queueId + " element " + element + " for " + src);
if (this.mediaPromises[queueId]) {
this.mediaPromises[queueId] =
this.mediaPromises[queueId].then(function() {
console.log("previous promise completed for " + queueId);
element.src = src;
}, function() {
console.log("previous promise failed for " + queueId);
element.src = src;
});
}
else {
element.src = src;
}
};
/**
* Retrieve the local <code>&lt;video&gt;</code> DOM element.
* @return {Element} The dom element
@@ -180,13 +266,15 @@ MatrixCall.prototype.setLocalVideoElement = function(element) {
if (element && this.localAVStream && this.type === 'video') {
element.autoplay = true;
element.src = this.URL.createObjectURL(this.localAVStream);
this.assignElement(element,
this.URL.createObjectURL(this.localAVStream),
"localVideo");
element.muted = true;
var self = this;
setTimeout(function() {
var vel = self.getLocalVideoElement();
if (vel.play) {
vel.play();
self.playElement(vel, "localVideo");
}
}, 0);
}
@@ -205,9 +293,11 @@ MatrixCall.prototype.setRemoteVideoElement = function(element) {
/**
* Set the remote <code>&lt;audio&gt;</code> DOM element. If this call is active,
* the first received audio-only stream will be rendered to it immediately.
* The audio will *not* be rendered from the remoteVideoElement.
* @param {Element} element The <code>&lt;video&gt;</code> DOM element.
*/
MatrixCall.prototype.setRemoteAudioElement = function(element) {
this.remoteVideoElement.muted = true;
this.remoteAudioElement = element;
_tryPlayRemoteAudioStream(this);
};
@@ -414,16 +504,20 @@ MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
videoEl.autoplay = true;
if (this.screenSharingStream) {
debuglog("Setting screen sharing stream to the local video element");
videoEl.src = this.URL.createObjectURL(this.screenSharingStream);
this.assignElement(videoEl,
this.URL.createObjectURL(this.screenSharingStream),
"localVideo");
}
else {
videoEl.src = this.URL.createObjectURL(stream);
this.assignElement(videoEl,
this.URL.createObjectURL(stream),
"localVideo");
}
videoEl.muted = true;
setTimeout(function() {
var vel = self.getLocalVideoElement();
if (vel.play) {
vel.play();
self.playElement(vel, "localVideo");
}
}, 0);
}
@@ -460,12 +554,14 @@ MatrixCall.prototype._gotUserMediaForAnswer = function(stream) {
if (localVidEl && self.type == 'video') {
localVidEl.autoplay = true;
localVidEl.src = self.URL.createObjectURL(stream);
this.assignElement(localVidEl,
this.URL.createObjectURL(stream),
"localVideo");
localVidEl.muted = true;
setTimeout(function() {
var vel = self.getLocalVideoElement();
if (vel.play) {
vel.play();
self.playElement(vel, "localVideo");
}
}, 0);
}
@@ -700,6 +796,7 @@ MatrixCall.prototype._onAddStream = function(event) {
if (s.getVideoTracks().length > 0) {
this.type = 'video';
this.remoteAVStream = s;
this.remoteAStream = s;
} else {
this.type = 'voice';
this.remoteAStream = s;
@@ -712,12 +809,20 @@ MatrixCall.prototype._onAddStream = function(event) {
t.onstarted = hookCallback(self, self._onRemoteStreamTrackStarted);
});
event.stream.onended = hookCallback(self, self._onRemoteStreamEnded);
if (event.stream.oninactive !== undefined) {
event.stream.oninactive = hookCallback(self, self._onRemoteStreamEnded);
}
else {
// onended is deprecated from Chrome 54
event.stream.onended = hookCallback(self, self._onRemoteStreamEnded);
}
// not currently implemented in chrome
event.stream.onstarted = hookCallback(self, self._onRemoteStreamStarted);
if (this.type === 'video') {
_tryPlayRemoteStream(this);
_tryPlayRemoteAudioStream(this);
}
else {
_tryPlayRemoteAudioStream(this);
@@ -824,21 +929,21 @@ var sendCandidate = function(self, content) {
var terminate = function(self, hangupParty, hangupReason, shouldEmit) {
if (self.getRemoteVideoElement()) {
if (self.getRemoteVideoElement().pause) {
self.getRemoteVideoElement().pause();
self.pauseElement(self.getRemoteVideoElement(), "remoteVideo");
}
self.getRemoteVideoElement().src = "";
self.assignElement(self.getRemoteVideoElement(), "", "remoteVideo");
}
if (self.getRemoteAudioElement()) {
if (self.getRemoteAudioElement().pause) {
self.getRemoteAudioElement().pause();
self.pauseElement(self.getRemoteAudioElement(), "remoteAudio");
}
self.getRemoteAudioElement().src = "";
self.assignElement(self.getRemoteAudioElement(), "", "remoteAudio");
}
if (self.getLocalVideoElement()) {
if (self.getLocalVideoElement().pause) {
self.getLocalVideoElement().pause();
self.pauseElement(self.getLocalVideoElement(), "localVideo");
}
self.getLocalVideoElement().src = "";
self.assignElement(self.getLocalVideoElement(), "", "localVideo");
}
self.hangupParty = hangupParty;
self.hangupReason = hangupReason;
@@ -896,11 +1001,13 @@ var _tryPlayRemoteStream = function(self) {
if (self.getRemoteVideoElement() && self.remoteAVStream) {
var player = self.getRemoteVideoElement();
player.autoplay = true;
player.src = self.URL.createObjectURL(self.remoteAVStream);
self.assignElement(player,
self.URL.createObjectURL(self.remoteAVStream),
"remoteVideo");
setTimeout(function() {
var vel = self.getRemoteVideoElement();
if (vel.play) {
vel.play();
self.playElement(vel, "remoteVideo");
}
// OpenWebRTC does not support oniceconnectionstatechange yet
if (self.webRtc.isOpenWebRTC()) {
@@ -914,11 +1021,13 @@ var _tryPlayRemoteAudioStream = function(self) {
if (self.getRemoteAudioElement() && self.remoteAStream) {
var player = self.getRemoteAudioElement();
player.autoplay = true;
player.src = self.URL.createObjectURL(self.remoteAStream);
self.assignElement(player,
self.URL.createObjectURL(self.remoteAStream),
"remoteAudio");
setTimeout(function() {
var ael = self.getRemoteAudioElement();
if (ael.play) {
ael.play();
self.playElement(ael, "remoteAudio");
}
// OpenWebRTC does not support oniceconnectionstatechange yet
if (self.webRtc.isOpenWebRTC()) {
@@ -1102,6 +1211,7 @@ var forAllTracksOnStream = function(s, f) {
/** The MatrixCall class. */
module.exports.MatrixCall = MatrixCall;
/**
* Create a new Matrix call for the browser.
* @param {MatrixClient} client The client instance to use.
+31 -10
View File
@@ -1,18 +1,17 @@
{
"name": "matrix-js-sdk",
"version": "0.5.2",
"version": "0.7.0",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"test": "istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --captureExceptions",
"check": "jasmine-node spec --verbose --junitreport --captureExceptions",
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
"build": "jshint -c .jshint lib/ && browserify browser-index.js -o dist/browser-matrix-dev.js --ignore-missing",
"watch": "watchify browser-index.js -o dist/browser-matrix-dev.js -v",
"build": "jshint -c .jshint lib/ && rimraf dist && mkdir dist && browserify --exclude olm browser-index.js -o dist/browser-matrix.js --ignore-missing && uglifyjs -c -m -o dist/browser-matrix.min.js dist/browser-matrix.js",
"dist": "npm run build",
"watch": "watchify --exclude olm browser-index.js -o dist/browser-matrix-dev.js -v",
"lint": "jshint -c .jshint lib spec && gjslint --unix_mode --disable 0131,0211,0200,0222,0212 --max_line_length 90 -r spec/ -r lib/",
"release": "npm run build && mkdir -p dist/$npm_package_version && uglifyjs -c -m -o dist/$npm_package_version/browser-matrix-$npm_package_version.min.js dist/browser-matrix-dev.js && cp dist/browser-matrix-dev.js dist/$npm_package_version/browser-matrix-$npm_package_version.js",
"prepublish": "git rev-parse HEAD > git-revision.txt",
"version": "npm run release && git add dist/$npm_package_version"
"prepublish": "git rev-parse HEAD > git-revision.txt"
},
"repository": {
"url": "https://github.com/matrix-org/matrix-js-sdk"
@@ -22,19 +21,41 @@
],
"browser": "browser-index.js",
"author": "matrix.org",
"license": "Apache 2.0",
"license": "Apache-2.0",
"files": [
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
"README.md",
"RELEASING.md",
"examples",
"git-hooks",
"git-revision.txt",
"index.js",
"browser-index.js",
"jenkins.sh",
"lib",
"package.json",
"release.sh",
"spec"
],
"dependencies": {
"another-json": "^0.2.0",
"browser-request": "^0.3.3",
"browserify": "^10.2.3",
"q": "^1.4.1",
"request": "^2.53.0"
},
"devDependencies": {
"watchify": "^3.2.1",
"istanbul": "^0.3.13",
"jasmine-node": "^1.14.5",
"jshint": "^2.8.0",
"jsdoc": "^3.4.0",
"uglifyjs": "^2.4.10"
"jshint": "^2.8.0",
"rimraf": "^2.5.4",
"uglifyjs": "^2.4.10",
"watchify": "^3.2.1"
},
"optionalDependencies": {
"olm": "https://matrix.org/packages/npm/olm/olm-1.3.0.tgz"
}
}
+90 -8
View File
@@ -1,13 +1,18 @@
#!/bin/sh
#!/bin/bash
#
# Script to perform a release of matrix-js-sdk. Performs the steps documented
# in RELEASING.md
#
# Requires github-changelog-generator; to install, do
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
# Requires:
# github-changelog-generator; to install, do
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
# jq; install from your distibution's package manager (https://stedolan.github.io/jq/)
set -e
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
hub --version > /dev/null || (echo "hub is required: please install it"; kill $$)
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
help() {
@@ -20,6 +25,13 @@ $USAGE
EOF
}
ret=0
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
if [ "$ret" -eq 0 ]; then
echo "package.json contains develop dependencies. Refusing to release."
exit
fi
skip_changelog=
skip_jsdoc=
changelog_file="CHANGELOG.md"
@@ -47,11 +59,27 @@ if [ $# -ne 1 ]; then
exit 1
fi
if [ -z "$skip_changelog" ]; then
# update_changelog doesn't have a --version flag
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
fi
# ignore leading v on release
release="${1#v}"
tag="v${release}"
rel_branch="release-$tag"
prerelease=0
# We check if this build is a prerelease by looking to
# see if the version has a hyphen in it. Crude,
# but semver doesn't support postreleases so anything
# with a hyphen is a prerelease.
echo $release | grep -q '-' && prerelease=1
if [ $prerelease -eq 1 ]; then
echo Making a PRE-RELEASE
fi
if [ -z "$skip_changelog" ]; then
if ! command -v update_changelog >/dev/null 2>&1; then
echo "release.sh requires github-changelog-generator. Try:" >&2
@@ -61,9 +89,16 @@ if [ -z "$skip_changelog" ]; then
fi
# we might already be on the release branch, in which case, yay
if [ $(git symbolic-ref --short HEAD) != "$rel_branch" ]; then
# If we're on any branch starting with 'release', we don't create
# a separate release branch (this allows us to use the same
# release branch for releases and release candidates).
curbranch=$(git symbolic-ref --short HEAD)
if [[ "$curbranch" != release* ]]; then
echo "Creating release branch"
git checkout -b "$rel_branch"
else
echo "Using current branch ($curbranch) for release"
rel_branch=$curbranch
fi
if [ -z "$skip_changelog" ]; then
@@ -81,7 +116,54 @@ set -x
# Bump package.json, build the dist, and tag
echo "npm version"
npm version "$release"
# npm version will automatically commit its modification
# and make a release tag. We don't want it to create the tag
# because github will do that, but we can only turn off both
# of these behaviours, so we have to manually commit the
# result.
npm version --no-git-tag-version "$release"
git commit package.json -m "$tag"
# If there is a 'dist' script in the package.json,
# run it in a separate checkout of the project, then
# upload any files in the 'dist' directory as release
# assets.
# We make a completely separate checkout to be sure
# we're using released versions of the dependencies
# (rather than whatever we're pulling in from npm link)
assets=''
dodist=0
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
if [ $dodist -eq 0 ]; then
projdir=`pwd`
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
echo "Building distribution copy in $builddir"
pushd "$builddir"
git clone "$projdir" .
git checkout "$rel_branch"
npm install
# We haven't tagged yet, so tell the dist script what version
# it's building
DIST_VERSION="$tag" npm run dist
popd
for i in "$builddir"/dist/*; do
assets="$assets -a $i"
done
fi
# push the release branch (github can't release from
# a branch it doesn't have)
git push origin "$rel_branch"
hubflags=''
if [ $prerelease -eq 1 ]; then
hubflags='-p'
fi
hub release create $hubflags $assets -m "$tag" "$tag"
if [ $dodist -eq 0 ]; then
rm -rf "$builddir"
fi
if [ -z "$skip_jsdoc" ]; then
echo "generating jsdocs"
@@ -90,7 +172,7 @@ if [ -z "$skip_jsdoc" ]; then
echo "copying jsdocs to gh-pages branch"
git checkout gh-pages
git pull
cp -ar ".jsdoc/matrix-js-sdk/$release" .
cp -a ".jsdoc/matrix-js-sdk/$release" .
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' \
$release index.html
@@ -104,8 +186,8 @@ git checkout master
git pull
git merge --ff-only "$rel_branch"
# push everything to github
git push origin master "$rel_branch" "$tag"
# push master and docs (if generated) to github
git push origin master
if [ -z "$skip_jsdoc" ]; then
git push origin gh-pages
fi
+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;
+716 -196
View File
@@ -1,249 +1,769 @@
"use strict";
var sdk = require("../..");
var q = require("q");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
function MockStorageApi() {
this.data = {};
var utils = require("../../lib/utils");
var test_utils = require("../test-utils");
var aliHttpBackend;
var bobHttpBackend;
var aliClient;
var roomId = "!room:localhost";
var aliUserId = "@ali:localhost";
var aliDeviceId = "zxcvb";
var aliAccessToken = "aseukfgwef";
var bobClient;
var bobUserId = "@bob:localhost";
var bobDeviceId = "bvcxz";
var bobAccessToken = "fewgfkuesa";
var bobOneTimeKeys;
var aliDeviceKeys;
var bobDeviceKeys;
var bobDeviceCurve25519Key;
var bobDeviceEd25519Key;
var aliStorage;
var bobStorage;
var aliMessages;
var bobMessages;
/**
* Set an expectation that the client will upload device keys and a number of
* one-time keys; then flush the http requests.
*
* @param {string} deviceId expected device id in upload request
* @param {object} httpBackend
*
* @return {promise} completes once the http requests have completed, returning combined
* {one_time_keys: {}, device_keys: {}}
*/
function expectKeyUpload(deviceId, httpBackend) {
var uploadPath = "/keys/upload/" + deviceId;
var keys = {};
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.one_time_keys).not.toBeDefined();
expect(content.device_keys).toBeDefined();
keys.device_keys = content.device_keys;
return {one_time_key_counts: {signed_curve25519: 0}};
});
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.device_keys).not.toBeDefined();
expect(content.one_time_keys).toBeDefined();
expect(content.one_time_keys).not.toEqual({});
var count = 0;
for (var key in content.one_time_keys) {
if (content.one_time_keys.hasOwnProperty(key)) {
count++;
}
}
expect(count).toEqual(5);
keys.one_time_keys = content.one_time_keys;
return {one_time_key_counts: {signed_curve25519: count}};
});
return httpBackend.flush(uploadPath, 2).then(function() {
return keys;
});
}
MockStorageApi.prototype = {
setItem: function(k, v) {
this.data[k] = v;
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
}
};
/**
* Set an expectation that ali will upload device keys and a number of one-time keys;
* then flush the http requests.
*
* <p>Updates <tt>aliDeviceKeys</tt>
*
* @return {promise} completes once the http requests have completed.
*/
function expectAliKeyUpload() {
return expectKeyUpload(aliDeviceId, aliHttpBackend).then(function(content) {
aliDeviceKeys = content.device_keys;
});
}
/**
* Set an expectation that bob will upload device keys and a number of one-time keys;
* then flush the http requests.
*
* <p>Updates <tt>bobDeviceKeys</tt>, <tt>bobOneTimeKeys</tt>,
* <tt>bobDeviceCurve25519Key</tt>, <tt>bobDeviceEd25519Key</tt>
*
* @return {promise} completes once the http requests have completed.
*/
function expectBobKeyUpload() {
return expectKeyUpload(bobDeviceId, bobHttpBackend).then(function(content) {
bobDeviceKeys = content.device_keys;
bobOneTimeKeys = content.one_time_keys;
expect(bobDeviceKeys).toBeDefined();
expect(bobOneTimeKeys).toBeDefined();
bobDeviceCurve25519Key = bobDeviceKeys.keys["curve25519:bvcxz"];
bobDeviceEd25519Key = bobDeviceKeys.keys["ed25519:bvcxz"];
});
}
function bobUploadsKeys() {
bobClient.uploadKeys(5).catch(test_utils.failTest);
return expectBobKeyUpload();
}
/**
* Set an expectation that ali will query bobs keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
*/
function expectAliQueryKeys() {
// can't query keys before bob has uploaded them
expect(bobDeviceKeys).toBeDefined();
var bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual({});
var result = {};
result[bobUserId] = bobKeys;
return {device_keys: result};
});
return aliHttpBackend.flush("/keys/query", 1);
}
/**
* Set an expectation that bob will query alis keys; then flush the http request.
*
* @return {promise} which resolves once the http request has completed.
*/
function expectBobQueryKeys() {
// can't query keys before ali has uploaded them
expect(aliDeviceKeys).toBeDefined();
var aliKeys = {};
aliKeys[aliDeviceId] = aliDeviceKeys;
bobHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[aliUserId]).toEqual({});
var result = {};
result[aliUserId] = aliKeys;
return {device_keys: result};
});
return bobHttpBackend.flush("/keys/query", 1);
}
/**
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
*/
function expectAliClaimKeys() {
// can't query keys before bob has uploaded them
expect(bobOneTimeKeys).toBeDefined();
aliHttpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
var claimType = content.one_time_keys[bobUserId][bobDeviceId];
expect(claimType).toEqual("signed_curve25519");
for (var keyId in bobOneTimeKeys) {
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
if (keyId.indexOf(claimType + ":") === 0) {
break;
}
}
}
var result = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = bobOneTimeKeys[keyId];
return {one_time_keys: result};
});
return aliHttpBackend.flush("/keys/claim", 1);
}
function aliDownloadsKeys() {
// can't query keys before bob has uploaded them
expect(bobDeviceEd25519Key).toBeDefined();
var p1 = aliClient.downloadKeys([bobUserId]).then(function() {
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([{
id: "bvcxz",
key: bobDeviceEd25519Key,
verified: false,
blocked: false,
display_name: null,
}]);
});
var p2 = expectAliQueryKeys();
// check that the localStorage is updated as we expect (not sure this is
// an integration test, but meh)
return q.all([p1, p2]).then(function() {
var devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
expect(devices[bobDeviceId].keys).toEqual(bobDeviceKeys.keys);
expect(devices[bobDeviceId].verified).
toBe(0); // DeviceVerification.UNVERIFIED
});
}
function aliEnablesEncryption() {
return aliClient.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
}).then(function() {
expect(aliClient.isRoomEncrypted(roomId)).toBeTruthy();
});
}
function bobEnablesEncryption() {
return bobClient.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
}).then(function() {
expect(bobClient.isRoomEncrypted(roomId)).toBeTruthy();
});
}
/**
* Ali sends a message, first claiming e2e keys. Set the expectations and
* check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function aliSendsFirstMessage() {
return q.all([
sendMessage(aliClient),
expectAliQueryKeys()
.then(expectAliClaimKeys)
.then(expectAliSendMessageRequest)
]).spread(function(_, ciphertext) {
return ciphertext;
});
}
/**
* Ali sends a message without first claiming e2e keys. Set the expectations
* and check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function aliSendsMessage() {
return q.all([
sendMessage(aliClient),
expectAliSendMessageRequest()
]).spread(function(_, ciphertext) {
return ciphertext;
});
}
/**
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
* expectations and check the results.
*
* @return {promise} which resolves to the ciphertext for Ali's device.
*/
function bobSendsReplyMessage() {
return q.all([
sendMessage(bobClient),
expectBobQueryKeys()
.then(expectBobSendMessageRequest)
]).spread(function(_, ciphertext) {
return ciphertext;
});
}
/**
* Set an expectation that Ali will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function expectAliSendMessageRequest() {
return expectSendMessageRequest(aliHttpBackend).then(function(content) {
aliMessages.push(content);
expect(utils.keys(content.ciphertext)).toEqual([bobDeviceCurve25519Key]);
var ciphertext = content.ciphertext[bobDeviceCurve25519Key];
expect(ciphertext).toBeDefined();
return ciphertext;
});
}
/**
* Set an expectation that Bob will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function expectBobSendMessageRequest() {
return expectSendMessageRequest(bobHttpBackend).then(function(content) {
bobMessages.push(content);
var aliKeyId = "curve25519:" + aliDeviceId;
var aliDeviceCurve25519Key = aliDeviceKeys.keys[aliKeyId];
expect(utils.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
var ciphertext = content.ciphertext[aliDeviceCurve25519Key];
expect(ciphertext).toBeDefined();
return ciphertext;
});
}
function sendMessage(client) {
return client.sendMessage(
roomId, {msgtype: "m.text", body: "Hello, World"}
);
}
function expectSendMessageRequest(httpBackend) {
var path = "/send/m.room.encrypted/";
var sent;
httpBackend.when("PUT", path).respond(200, function(path, content) {
sent = content;
return {
event_id: "asdfgh",
};
});
return httpBackend.flush(path, 1).then(function() {
return sent;
});
}
function aliRecvMessage() {
var message = bobMessages.shift();
return recvMessage(aliHttpBackend, aliClient, bobUserId, message);
}
function bobRecvMessage() {
var message = aliMessages.shift();
return recvMessage(bobHttpBackend, bobClient, aliUserId, message);
}
function recvMessage(httpBackend, client, sender, message) {
var syncData = {
next_batch: "x",
rooms: {
join: {
}
}
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
test_utils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: sender,
})
]
}
};
httpBackend.when("GET", "/sync").respond(200, syncData);
var deferred = q.defer();
var onEvent = function(event) {
console.log(client.credentials.userId + " received event",
event);
// ignore the m.room.member events
if (event.getType() == "m.room.member") {
return;
}
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toEqual({
msgtype: "m.text",
body: "Hello, World"
});
expect(event.isEncrypted()).toBeTruthy();
client.removeListener("event", onEvent);
deferred.resolve();
};
client.on("event", onEvent);
httpBackend.flush();
return deferred.promise;
}
function aliStartClient() {
expectAliKeyUpload().catch(test_utils.failTest);
startClient(aliHttpBackend, aliClient);
return aliHttpBackend.flush().then(function() {
console.log("Ali client started");
});
}
function bobStartClient() {
expectBobKeyUpload().catch(test_utils.failTest);
startClient(bobHttpBackend, bobClient);
return bobHttpBackend.flush().then(function() {
console.log("Bob client started");
});
}
/**
* Set http responses for the requests which are made when a client starts, and
* start the client.
*
* @param {object} httpBackend
* @param {MatrixClient} client
*/
function startClient(httpBackend, client) {
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
// send a sync response including our test room.
var syncData = {
next_batch: "x",
rooms: {
join: { }
}
};
syncData.rooms.join[roomId] = {
state: {
events: [
test_utils.mkMembership({
mship: "join",
user: aliUserId,
}),
test_utils.mkMembership({
mship: "join",
user: bobUserId,
}),
]
},
timeline: {
events: []
}
};
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
}
describe("MatrixClient crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
var baseUrl = "http://localhost.or.something";
var httpBackend;
var aliClient;
var roomId = "!room:localhost";
var aliUserId = "@ali:localhost";
var aliDeviceId = "zxcvb";
var aliAccessToken = "aseukfgwef";
var bobClient;
var bobUserId = "@bob:localhost";
var bobDeviceId = "bvcxz";
var bobAccessToken = "fewgfkuesa";
var bobOneTimeKeys;
var bobDeviceKeys;
var bobDeviceCurve25519Key;
var bobDeviceEd25519Key;
var aliLocalStore;
var aliStorage;
var bobStorage;
var aliMessage;
beforeEach(function() {
aliLocalStore = new MockStorageApi();
aliStorage = new sdk.WebStorageSessionStore(aliLocalStore);
bobStorage = new sdk.WebStorageSessionStore(new MockStorageApi());
utils.beforeEach(this);
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
test_utils.beforeEach(this);
aliStorage = new sdk.WebStorageSessionStore(new test_utils.MockStorageApi());
aliHttpBackend = new HttpBackend();
aliClient = sdk.createClient({
baseUrl: baseUrl,
baseUrl: "http://alis.server",
userId: aliUserId,
accessToken: aliAccessToken,
deviceId: aliDeviceId,
sessionStore: aliStorage
sessionStore: aliStorage,
request: aliHttpBackend.requestFn,
});
bobStorage = new sdk.WebStorageSessionStore(new test_utils.MockStorageApi());
bobHttpBackend = new HttpBackend();
bobClient = sdk.createClient({
baseUrl: baseUrl,
baseUrl: "http://bobs.server",
userId: bobUserId,
accessToken: bobAccessToken,
deviceId: bobDeviceId,
sessionStore: bobStorage
sessionStore: bobStorage,
request: bobHttpBackend.requestFn,
});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
bobOneTimeKeys = undefined;
aliDeviceKeys = undefined;
bobDeviceKeys = undefined;
bobDeviceCurve25519Key = undefined;
bobDeviceEd25519Key = undefined;
aliMessages = [];
bobMessages = [];
});
describe("Ali account setup", function() {
it("should have device keys", function(done) {
expect(aliClient.deviceKeys).toBeDefined();
expect(aliClient.deviceKeys.user_id).toEqual(aliUserId);
expect(aliClient.deviceKeys.device_id).toEqual(aliDeviceId);
done();
});
it("should have a curve25519 key", function(done) {
expect(aliClient.deviceCurve25519Key).toBeDefined();
done();
});
afterEach(function() {
aliClient.stopClient();
bobClient.stopClient();
});
function bobUploadsKeys(done) {
var uploadPath = "/keys/upload/bvcxz";
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.one_time_keys).toEqual({});
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.one_time_keys).not.toEqual({});
bobDeviceKeys = content.device_keys;
bobOneTimeKeys = content.one_time_keys;
var count = 0;
for (var key in content.one_time_keys) {
if (content.one_time_keys.hasOwnProperty(key)) {
count++;
}
it('Ali knows the difference between a new user and one with no devices',
function(done) {
aliHttpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:id': {},
}
expect(count).toEqual(5);
return {one_time_key_counts: {curve25519: count}};
});
return {one_time_key_counts: {}};
});
bobClient.uploadKeys(5);
httpBackend.flush().done(function() {
expect(bobDeviceKeys).toBeDefined();
expect(bobOneTimeKeys).toBeDefined();
bobDeviceCurve25519Key = bobDeviceKeys.keys["curve25519:bvcxz"];
bobDeviceEd25519Key = bobDeviceKeys.keys["ed25519:bvcxz"];
done();
});
}
it("Bob uploads without one-time keys and with one-time keys", bobUploadsKeys);
var p1 = aliClient.downloadKeys(['@bob:id']);
var p2 = aliHttpBackend.flush('/keys/query', 1);
q.all([p1, p2]).then(function() {
var devices = aliStorage.getEndToEndDevicesForUser('@bob:id');
expect(utils.keys(devices).length).toEqual(0);
// request again: should be no more requests
return aliClient.downloadKeys(['@bob:id']);
}).nodeify(done);
}
);
it("Bob uploads without one-time keys and with one-time keys", function(done) {
q()
.then(bobUploadsKeys)
.catch(test_utils.failTest).done(done);
});
it("Ali downloads Bobs keys", function(done) {
q()
.then(bobUploadsKeys)
.then(aliDownloadsKeys)
.catch(test_utils.failTest).done(done);
});
it("Ali gets keys with an invalid signature", function(done) {
q()
.then(bobUploadsKeys)
.then(function() {
// tamper bob's keys!
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeDefined();
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
return q.all(aliClient.downloadKeys([bobUserId]),
expectAliQueryKeys());
})
.then(function() {
// should get an empty list
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
})
.catch(test_utils.failTest).done(done);
});
it("Ali gets keys with an incorrect userId", function(done) {
var eveUserId = "@eve:localhost";
var bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'bvcxz',
keys: {
'ed25519:bvcxz': 'pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q',
'curve25519:bvcxz': '7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ',
},
user_id: '@eve:localhost',
signatures: {
'@eve:localhost': {
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
},
},
};
function aliDownloadsKeys(done) {
var bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
httpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual({});
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
var result = {};
result[bobUserId] = bobKeys;
return {device_keys: result};
});
aliClient.downloadKeys([bobUserId]).then(function() {
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([{
id: "bvcxz",
key: bobDeviceEd25519Key
}]);
});
httpBackend.flush().done(function() {
var devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
expect(devices).toEqual(bobKeys);
done();
});
}
it("Ali downloads Bobs keys", function(done) {
bobUploadsKeys(function() {aliDownloadsKeys(done);});
q.all(
aliClient.downloadKeys([bobUserId, eveUserId]),
aliHttpBackend.flush("/keys/query", 1)
).then(function() {
// should get an empty list
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
expect(aliClient.listDeviceKeys(eveUserId)).toEqual([]);
}).catch(test_utils.failTest).done(done);
});
function aliEnablesEncryption(done) {
httpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
expect(content.one_time_keys[bobUserId][bobDeviceId]).toEqual("curve25519");
for (var keyId in bobOneTimeKeys) {
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
if (keyId.indexOf("curve25519:") === 0) {
break;
}
}
}
it("Ali gets keys with an incorrect deviceId", function(done) {
var bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'bad_device',
keys: {
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0',
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc',
},
user_id: '@bob:localhost',
signatures: {
'@bob:localhost': {
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
},
},
};
var bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
var result = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = bobOneTimeKeys[keyId];
return {one_time_keys: result};
result[bobUserId] = bobKeys;
return {device_keys: result};
});
aliClient.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
members: [aliUserId, bobUserId]
}).then(function(res) {
expect(res.missingUsers).toEqual([]);
expect(res.missingDevices).toEqual({});
expect(aliClient.isRoomEncrypted(roomId)).toBeTruthy();
done();
});
httpBackend.flush();
}
q.all(
aliClient.downloadKeys([bobUserId]),
aliHttpBackend.flush("/keys/query", 1)
).then(function() {
// should get an empty list
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
}).catch(test_utils.failTest).done(done);
});
it("Ali enables encryption", function(done) {
bobUploadsKeys(function() {
aliDownloadsKeys(function() {
aliEnablesEncryption(done);
});
});
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.catch(test_utils.failTest).done(done);
});
function aliSendsMessage(done) {
var txnId = "a.transaction.id";
var path = "/send/m.room.encrypted/" + txnId;
httpBackend.when("PUT", path).respond(200, function(path, content) {
aliMessage = content;
expect(aliMessage.ciphertext[bobDeviceCurve25519Key]).toBeDefined();
return {};
});
aliClient.sendMessage(
roomId, {msgtype: "m.text", body: "Hello, World"}, txnId
);
httpBackend.flush().done(function() {done();});
}
it("Ali sends a message", function(done) {
bobUploadsKeys(function() {
aliDownloadsKeys(function() {
aliEnablesEncryption(function() {
aliSendsMessage(done);
});
});
});
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.catch(test_utils.failTest).nodeify(done);
});
function bobRecvMessage(done) {
var syncData = {
next_batch: "x",
rooms: {
join: {
}
}
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
utils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: aliMessage
})
]
}
};
httpBackend.when("GET", "/sync").respond(200, syncData);
bobClient.on("event", function(event) {
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toEqual({
msgtype: "m.text",
body: "Hello, World"
});
expect(event.isEncrypted()).toBeTruthy();
done();
});
bobClient.startClient();
httpBackend.flush();
}
it("Bob receives a message", function(done) {
bobUploadsKeys(function() {
aliDownloadsKeys(function() {
aliEnablesEncryption(function() {
aliSendsMessage(function() {
bobRecvMessage(done);
});
});
});
});
}, 30000); //timeout after 30s
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobStartClient)
.then(bobRecvMessage)
.catch(test_utils.failTest).done(done);
});
it("Bob receives a message with a bogus sender", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobStartClient)
.then(function() {
var message = aliMessages.shift();
var syncData = {
next_batch: "x",
rooms: {
join: {
}
}
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
test_utils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: "@bogus:sender",
})
]
}
};
bobHttpBackend.when("GET", "/sync").respond(200, syncData);
var deferred = q.defer();
var onEvent = function(event) {
console.log(bobClient.credentials.userId + " received event",
event);
// ignore the m.room.member events
if (event.getType() == "m.room.member") {
return;
}
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
expect(event.isEncrypted()).toBeTruthy();
bobClient.removeListener("event", onEvent);
deferred.resolve();
};
bobClient.on("event", onEvent);
bobHttpBackend.flush();
return deferred.promise;
})
.catch(test_utils.failTest).done(done);
});
it("Ali blocks Bob's device", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliDownloadsKeys)
.then(function() {
aliClient.setDeviceBlocked(bobUserId, bobDeviceId, true);
var p1 = sendMessage(aliClient);
var p2 = expectAliQueryKeys()
.then(expectAliClaimKeys)
.then(function() {
return expectSendMessageRequest(aliHttpBackend);
}).then(function(sentContent) {
// no unblocked devices, so the ciphertext should be empty
expect(sentContent.ciphertext).toEqual({});
});
return q.all([p1, p2]);
}).catch(test_utils.failTest).nodeify(done);
});
it("Bob receives two pre-key messages", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobStartClient)
.then(bobRecvMessage)
.then(aliSendsMessage)
.then(bobRecvMessage)
.catch(test_utils.failTest).done(done);
});
it("Bob replies to the message", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobStartClient)
.then(bobRecvMessage)
.then(bobEnablesEncryption)
.then(bobSendsReplyMessage).then(function(ciphertext) {
expect(ciphertext.type).toEqual(1);
}).then(aliRecvMessage)
.catch(test_utils.failTest).done(done);
});
it("Ali does a key query when she gets a new_device event", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(function() {
var syncData = {
next_batch: '2',
to_device: {
events: [
test_utils.mkEvent({
content: {
device_id: 'TEST_DEVICE',
rooms: [],
},
sender: bobUserId,
type: 'm.new_device',
}),
],
},
};
aliHttpBackend.when('GET', '/sync').respond(200, syncData);
return aliHttpBackend.flush('/sync', 1);
}).then(expectAliQueryKeys)
.nodeify(done);
});
});
+93 -16
View File
@@ -121,7 +121,8 @@ describe("getEventTimeline support", function() {
startClient(httpBackend, client
).then(function() {
var room = client.getRoom(roomId);
expect(function() { client.getEventTimeline(room, "event"); })
var timelineSet = room.getTimelineSets()[0];
expect(function() { client.getEventTimeline(timelineSet, "event"); })
.toThrow();
}).catch(utils.failTest).done(done);
});
@@ -137,7 +138,8 @@ describe("getEventTimeline support", function() {
startClient(httpBackend, client
).then(function() {
var room = client.getRoom(roomId);
expect(function() { client.getEventTimeline(room, "event"); })
var timelineSet = room.getTimelineSets()[0];
expect(function() { client.getEventTimeline(timelineSet, "event"); })
.not.toThrow();
}).catch(utils.failTest).done(done);
@@ -242,6 +244,7 @@ describe("MatrixClient event timelines", function() {
describe("getEventTimeline", function() {
it("should create a new timeline for new events", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
.respond(200, function() {
return {
@@ -257,7 +260,7 @@ describe("MatrixClient event timelines", function() {
};
});
client.getEventTimeline(room, "event1:bar").then(function(tl) {
client.getEventTimeline(timelineSet, "event1:bar").then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
for (var i = 0; i < 4; i++) {
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
@@ -274,6 +277,7 @@ describe("MatrixClient event timelines", function() {
it("should return existing timeline for known events", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
@@ -291,7 +295,7 @@ describe("MatrixClient event timelines", function() {
});
httpBackend.flush("/sync").then(function() {
return client.getEventTimeline(room, EVENTS[0].event_id);
return client.getEventTimeline(timelineSet, EVENTS[0].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(EVENTS[0]);
@@ -305,6 +309,7 @@ describe("MatrixClient event timelines", function() {
it("should update timelines where they overlap a previous /sync", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
@@ -335,7 +340,7 @@ describe("MatrixClient event timelines", function() {
});
client.on("sync", function() {
client.getEventTimeline(room, EVENTS[2].event_id
client.getEventTimeline(timelineSet, EVENTS[2].event_id
).then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
@@ -354,6 +359,7 @@ describe("MatrixClient event timelines", function() {
it("should join timelines where they overlap a previous /context",
function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
// with context which joins them all up.
@@ -410,19 +416,19 @@ describe("MatrixClient event timelines", function() {
});
var tl0, tl2, tl3;
client.getEventTimeline(room, EVENTS[0].event_id
client.getEventTimeline(timelineSet, EVENTS[0].event_id
).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl0 = tl;
return client.getEventTimeline(room, EVENTS[2].event_id);
return client.getEventTimeline(timelineSet, EVENTS[2].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl2 = tl;
return client.getEventTimeline(room, EVENTS[3].event_id);
return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl3 = tl;
return client.getEventTimeline(room, EVENTS[1].event_id);
return client.getEventTimeline(timelineSet, EVENTS[1].event_id);
}).then(function(tl) {
// we expect it to get merged in with event 2
expect(tl.getEvents().length).toEqual(2);
@@ -447,6 +453,7 @@ describe("MatrixClient event timelines", function() {
it("should fail gracefully if there is no event field", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
// with context which joins them all up.
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1")
@@ -460,7 +467,7 @@ describe("MatrixClient event timelines", function() {
};
});
client.getEventTimeline(room, "event1"
client.getEventTimeline(timelineSet, "event1"
).then(function(tl) {
// could do with a fail()
expect(true).toBeFalsy();
@@ -475,6 +482,7 @@ describe("MatrixClient event timelines", function() {
describe("paginateEventTimeline", function() {
it("should allow you to paginate backwards", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
@@ -503,7 +511,7 @@ describe("MatrixClient event timelines", function() {
});
var tl;
client.getEventTimeline(room, EVENTS[0].event_id
client.getEventTimeline(timelineSet, EVENTS[0].event_id
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(tl, {backwards: true});
@@ -525,6 +533,7 @@ describe("MatrixClient event timelines", function() {
it("should allow you to paginate forwards", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
@@ -553,7 +562,7 @@ describe("MatrixClient event timelines", function() {
});
var tl;
client.getEventTimeline(room, EVENTS[0].event_id
client.getEventTimeline(timelineSet, EVENTS[0].event_id
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(
@@ -607,10 +616,11 @@ describe("MatrixClient event timelines", function() {
it("should work when /send returns before /sync", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(room, event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
// 2 because the initial sync contained an event
expect(tl.getEvents().length).toEqual(2);
@@ -619,7 +629,7 @@ describe("MatrixClient event timelines", function() {
// now let the sync complete, and check it again
return httpBackend.flush("/sync", 1);
}).then(function() {
return client.getEventTimeline(room, event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
@@ -630,13 +640,14 @@ describe("MatrixClient event timelines", function() {
it("should work when /send returns after /sync", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
// initiate the send, and set up checks to be done when it completes
// - but note that it won't complete until after the /sync does, below.
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
console.log("sendTextMessage completed");
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(room, event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (2)");
expect(tl.getEvents().length).toEqual(2);
@@ -644,7 +655,7 @@ describe("MatrixClient event timelines", function() {
}).catch(utils.failTest).done(done);
httpBackend.flush("/sync", 1).then(function() {
return client.getEventTimeline(room, event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (1)");
expect(tl.getEvents().length).toEqual(2);
@@ -655,4 +666,70 @@ describe("MatrixClient event timelines", function() {
}).catch(utils.failTest);
});
});
it("should handle gappy syncs after redactions", function(done) {
// https://github.com/vector-im/vector-web/issues/1389
// a state event, followed by a redaction thereof
var event = utils.mkMembership({
room: roomId, mship: "join", user: otherUserId
});
var redaction = utils.mkEvent({
type: "m.room.redaction",
room_id: roomId,
sender: otherUserId,
content: {}
});
redaction.redacts = event.event_id;
var syncData = {
next_batch: "batch1",
rooms: {
join: {},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
event,
redaction,
],
limited: false,
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.flush().then(function() {
var room = client.getRoom(roomId);
var tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[1].isRedacted()).toBe(true);
var sync2 = {
next_batch: "batch2",
rooms: {
join: {},
},
};
sync2.rooms.join[roomId] = {
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "world"
}),
],
limited: true,
prev_batch: "newerTok",
},
};
httpBackend.when("GET", "/sync").respond(200, sync2);
return httpBackend.flush();
}).then(function() {
var room = client.getRoom(roomId);
var tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(1);
}).catch(utils.failTest).done(done);
});
});
+226 -2
View File
@@ -6,10 +6,11 @@ var Room = publicGlobals.Room;
var MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
var Filter = publicGlobals.Filter;
var utils = require("../test-utils");
var MockStorageApi = require("../MockStorageApi");
describe("MatrixClient", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend, store;
var client, httpBackend, store, sessionStore;
var userId = "@alice:localhost";
var accessToken = "aseukfgwef";
@@ -17,12 +18,18 @@ describe("MatrixClient", function() {
utils.beforeEach(this);
httpBackend = new HttpBackend();
store = new MatrixInMemoryStore();
var mockStorage = new MockStorageApi();
sessionStore = new sdk.WebStorageSessionStore(mockStorage);
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
deviceId: "aliceDevice",
accessToken: accessToken,
store: store
store: store,
sessionStore: sessionStore,
});
});
@@ -30,6 +37,117 @@ describe("MatrixClient", function() {
httpBackend.verifyNoOutstandingExpectation();
});
describe("uploadContent", function() {
var buf = new Buffer('hello world');
it("should upload the file", function(done) {
httpBackend.when(
"POST", "/_matrix/media/v1/upload"
).check(function(req) {
expect(req.data).toEqual(buf);
expect(req.queryParams.filename).toEqual("hi.txt");
expect(req.queryParams.access_token).toEqual(accessToken);
expect(req.headers["Content-Type"]).toEqual("text/plain");
expect(req.opts.json).toBeFalsy();
expect(req.opts.timeout).toBe(undefined);
}).respond(200, "content");
var prom = client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
});
expect(prom).toBeDefined();
var uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(1);
expect(uploads[0].promise).toBe(prom);
expect(uploads[0].loaded).toEqual(0);
prom.then(function(response) {
// for backwards compatibility, we return the raw JSON
expect(response).toEqual("content");
var uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(0);
}).catch(utils.failTest).done(done);
httpBackend.flush();
});
it("should parse the response if rawResponse=false", function(done) {
httpBackend.when(
"POST", "/_matrix/media/v1/upload"
).check(function(req) {
expect(req.opts.json).toBeFalsy();
}).respond(200, JSON.stringify({ "content_uri": "uri" }));
client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
}, {
rawResponse: false,
}).then(function(response) {
expect(response.content_uri).toEqual("uri");
}).catch(utils.failTest).done(done);
httpBackend.flush();
});
it("should parse errors into a MatrixError", function(done) {
// opts.json is false, so request returns unparsed json.
httpBackend.when(
"POST", "/_matrix/media/v1/upload"
).check(function(req) {
expect(req.data).toEqual(buf);
expect(req.opts.json).toBeFalsy();
}).respond(400, JSON.stringify({
"errcode": "M_SNAFU",
"error": "broken",
}));
client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
}).then(function(response) {
throw Error("request not failed");
}, function(error) {
expect(error.httpStatus).toEqual(400);
expect(error.errcode).toEqual("M_SNAFU");
expect(error.message).toEqual("broken");
}).catch(utils.failTest).done(done);
httpBackend.flush();
});
it("should return a promise which can be cancelled", function(done) {
var prom = client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
});
var uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(1);
expect(uploads[0].promise).toBe(prom);
expect(uploads[0].loaded).toEqual(0);
prom.then(function(response) {
throw Error("request not aborted");
}, function(error) {
expect(error).toEqual("aborted");
var uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(0);
}).catch(utils.failTest).done(done);
var r = client.cancelUpload(prom);
expect(r).toBe(true);
});
});
describe("joinRoom", function() {
it("should no-op if you've already joined a room", function() {
var roomId = "!foo:bar";
@@ -173,4 +291,110 @@ describe("MatrixClient", function() {
});
});
});
describe("downloadKeys", function() {
it("should do an HTTP request and then store the keys", function(done) {
var ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
// ed25519key = client.getDeviceEd25519Key();
var borisKeys = {
dev1: {
algorithms: ["1"],
device_id: "dev1",
keys: { "ed25519:dev1": ed25519key },
signatures: {
boris: {
"ed25519:dev1":
"RAhmbNDq1efK3hCpBzZDsKoGSsrHUxb25NW5/WbEV9R" +
"JVwLdP032mg5QsKt/pBDUGtggBcnk43n3nBWlA88WAw"
}
},
unsigned: { "abc": "def" },
user_id: "boris",
}
};
var chazKeys = {
dev2: {
algorithms: ["2"],
device_id: "dev2",
keys: { "ed25519:dev2": ed25519key },
signatures: {
chaz: {
"ed25519:dev2":
"FwslH/Q7EYSb7swDJbNB5PSzcbEO1xRRBF1riuijqvL" +
"EkrK9/XVN8jl4h7thGuRITQ01siBQnNmMK9t45QfcCQ"
}
},
unsigned: { "ghi": "def" },
user_id: "chaz",
}
};
/*
function sign(o) {
var anotherjson = require('another-json');
var b = JSON.parse(JSON.stringify(o));
delete(b.signatures);
delete(b.unsigned);
return client._crypto._olmDevice.sign(anotherjson.stringify(b));
};
console.log("Ed25519: " + ed25519key);
console.log("boris:", sign(borisKeys.dev1));
console.log("chaz:", sign(chazKeys.dev2));
*/
httpBackend.when("POST", "/keys/query").check(function(req) {
expect(req.data).toEqual({device_keys: {boris: {}, chaz: {}}});
}).respond(200, {
device_keys: {
boris: borisKeys,
chaz: chazKeys,
},
});
client.downloadKeys(["boris", "chaz"]).then(function(res) {
assertObjectContains(res.boris.dev1, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev1": ed25519key },
algorithms: ["1"],
unsigned: { "abc": "def" },
});
assertObjectContains(res.chaz.dev2, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev2" : ed25519key },
algorithms: ["2"],
unsigned: { "ghi": "def" },
});
}).catch(utils.failTest).done(done);
httpBackend.flush();
});
});
describe("deleteDevice", function() {
var auth = {a: 1};
it("should pass through an auth dict", function(done) {
httpBackend.when(
"DELETE", "/_matrix/client/unstable/devices/my_device"
).check(function(req) {
expect(req.data).toEqual({auth: auth});
}).respond(200);
client.deleteDevice(
"my_device", auth
).catch(utils.failTest).done(done);
httpBackend.flush();
});
});
});
function assertObjectContains(obj, expected) {
for (var k in expected) {
if (expected.hasOwnProperty(k)) {
expect(obj[k]).toEqual(expected[k]);
}
}
}
+14 -11
View File
@@ -66,20 +66,23 @@ describe("MatrixClient retrying", function() {
ev2 = tl[1];
expect(ev1.status).toEqual(EventStatus.SENDING);
expect(ev2.status).toEqual(EventStatus.QUEUED);
expect(ev2.status).toEqual(EventStatus.SENDING);
// now we can cancel the second and check everything looks sane
client.cancelPendingEvent(ev2);
expect(ev2.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(1);
// the first message should get sent, and the second should get queued
httpBackend.when("PUT", "/send/m.room.message/").check(function(rq) {
// ev2 should now have been queued
expect(ev2.status).toEqual(EventStatus.QUEUED);
// shouldn't be able to cancel the first message yet
expect(function() { client.cancelPendingEvent(ev1); })
.toThrow();
// now we can cancel the second and check everything looks sane
client.cancelPendingEvent(ev2);
expect(ev2.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(1);
// shouldn't be able to cancel the first message yet
expect(function() { client.cancelPendingEvent(ev1); })
.toThrow();
}).respond(400); // fail the first message
// fail the first send
httpBackend.when("PUT", "/send/m.room.message/")
.respond(400);
httpBackend.flush().then(function() {
expect(ev1.status).toEqual(EventStatus.NOT_SENT);
expect(tl.length).toEqual(1);
+617
View File
@@ -0,0 +1,617 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
try {
var Olm = require('olm');
} catch (e) {}
var anotherjson = require('another-json');
var q = require('q');
var sdk = require('../..');
var utils = require('../../lib/utils');
var test_utils = require('../test-utils');
var MockHttpBackend = require('../mock-request');
/**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
*
* @constructor
* @param {string} userId
* @param {string} deviceId
* @param {string} accessToken
*/
function TestClient(userId, deviceId, accessToken) {
this.userId = userId;
this.deviceId = deviceId;
this.storage = new sdk.WebStorageSessionStore(new test_utils.MockStorageApi());
this.httpBackend = new MockHttpBackend();
this.client = sdk.createClient({
baseUrl: "http://test.server",
userId: userId,
accessToken: accessToken,
deviceId: deviceId,
sessionStore: this.storage,
request: this.httpBackend.requestFn,
});
this.deviceKeys = null;
this.oneTimeKeys = [];
}
/**
* start the client, and wait for it to initialise.
*
* @return {Promise}
*/
TestClient.prototype.start = function() {
var self = this;
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
expect(content.one_time_keys).not.toBeDefined();
expect(content.device_keys).toBeDefined();
self.deviceKeys = content.device_keys;
return {one_time_key_counts: {signed_curve25519: 0}};
});
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
expect(content.device_keys).not.toBeDefined();
expect(content.one_time_keys).toBeDefined();
expect(content.one_time_keys).not.toEqual({});
self.oneTimeKeys = content.one_time_keys;
return {one_time_key_counts: {
signed_curve25519: utils.keys(self.oneTimeKeys).length
}};
});
this.client.startClient();
return this.httpBackend.flush();
};
/**
* stop the client
*/
TestClient.prototype.stop = function() {
this.client.stopClient();
};
/**
* get the uploaded curve25519 device key
*
* @return {string} base64 device key
*/
TestClient.prototype.getDeviceKey = function() {
var key_id = 'curve25519:' + this.deviceId;
return this.deviceKeys.keys[key_id];
};
/**
* get the uploaded ed25519 device key
*
* @return {string} base64 device key
*/
TestClient.prototype.getSigningKey = function() {
var key_id = 'ed25519:' + this.deviceId;
return this.deviceKeys.keys[key_id];
};
/**
* start an Olm session with a given recipient
*
* @param {Olm.Account} olmAccount
* @param {TestClient} recipientTestClient
* @return {Olm.Session}
*/
function createOlmSession(olmAccount, recipientTestClient) {
var otk_id = utils.keys(recipientTestClient.oneTimeKeys)[0];
var otk = recipientTestClient.oneTimeKeys[otk_id];
var session = new Olm.Session();
session.create_outbound(
olmAccount, recipientTestClient.getDeviceKey(), otk.key
);
return session;
}
/**
* encrypt an event with olm
*
* @param {object} opts
* @param {string=} opts.sender
* @param {string} opts.senderKey
* @param {Olm.Session} opts.p2pSession
* @param {TestClient} opts.recipient
* @param {object=} opts.plaincontent
* @param {string=} opts.plaintype
*
* @return {object} event
*/
function encryptOlmEvent(opts) {
expect(opts.senderKey).toBeDefined();
expect(opts.p2pSession).toBeDefined();
expect(opts.recipient).toBeDefined();
var plaintext = {
content: opts.plaincontent || {},
recipient: opts.recipient.userId,
recipient_keys: {
ed25519: opts.recipient.getSigningKey(),
},
sender: opts.sender || '@bob:xyz',
type: opts.plaintype || 'm.test',
};
var event = {
content: {
algorithm: 'm.olm.v1.curve25519-aes-sha2',
ciphertext: {},
sender_key: opts.senderKey,
},
sender: opts.sender || '@bob:xyz',
type: 'm.room.encrypted',
};
event.content.ciphertext[opts.recipient.getDeviceKey()] =
opts.p2pSession.encrypt(JSON.stringify(plaintext));
return event;
}
/**
* encrypt an event with megolm
*
* @param {object} opts
* @param {string} opts.senderKey
* @param {Olm.OutboundGroupSession} opts.groupSession
* @param {object=} opts.plaintext
* @param {string=} opts.room_id
*
* @return {object} event
*/
function encryptMegolmEvent(opts) {
expect(opts.senderKey).toBeDefined();
expect(opts.groupSession).toBeDefined();
var plaintext = opts.plaintext || {};
if (!plaintext.content) {
plaintext.content = {
body: '42',
msgtype: "m.text",
};
}
if (!plaintext.type) {
plaintext.type = "m.room.message";
}
if (!plaintext.room_id) {
expect(opts.room_id).toBeDefined();
plaintext.room_id = opts.room_id;
}
return {
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: opts.groupSession.encrypt(JSON.stringify(plaintext)),
device_id: "testDevice",
sender_key: opts.senderKey,
session_id: opts.groupSession.session_id(),
},
type: "m.room.encrypted",
};
}
/**
* build an encrypted room_key event to share a group session
*
* @param {object} opts
* @param {string} opts.senderKey
* @param {TestClient} opts.recipient
* @param {Olm.Session} opts.p2pSession
* @param {Olm.OutboundGroupSession} opts.groupSession
* @param {string=} opts.room_id
*
* @return {object} event
*/
function encryptGroupSessionKey(opts) {
return encryptOlmEvent({
senderKey: opts.senderKey,
recipient: opts.recipient,
p2pSession: opts.p2pSession,
plaincontent: {
algorithm: 'm.megolm.v1.aes-sha2',
room_id: opts.room_id,
session_id: opts.groupSession.session_id(),
session_key: opts.groupSession.session_key(),
},
plaintype: 'm.room_key',
});
}
describe("megolm", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
var ROOM_ID = "!room:id";
var testOlmAccount;
var testSenderKey;
var aliceTestClient;
var testDeviceKeys;
beforeEach(function(done) {
test_utils.beforeEach(this);
aliceTestClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs"
);
testOlmAccount = new Olm.Account();
testOlmAccount.create();
var testE2eKeys = JSON.parse(testOlmAccount.identity_keys());
testSenderKey = testE2eKeys.curve25519;
testDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'DEVICE_ID',
keys: {
'curve25519:DEVICE_ID': testE2eKeys.curve25519,
'ed25519:DEVICE_ID': testE2eKeys.ed25519,
},
user_id: '@bob:xyz',
};
var j = anotherjson.stringify(testDeviceKeys);
var sig = testOlmAccount.sign(j);
testDeviceKeys.signatures = {
'@bob:xyz': {
'ed25519:DEVICE_ID': sig,
},
};
return aliceTestClient.start().nodeify(done);
});
afterEach(function() {
aliceTestClient.stop();
});
it("Alice receives a megolm message", function(done) {
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
var groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
var roomKeyEncrypted = encryptGroupSessionKey({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a message with the group session
var messageEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
});
// Alice gets both the events in a single sync
var syncResponse = {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted],
},
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
timeline: {
events: [messageEncrypted],
},
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
return aliceTestClient.httpBackend.flush("/sync", 1).then(function() {
var room = aliceTestClient.client.getRoom(ROOM_ID);
var event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42');
}).nodeify(done);
});
it("Alice gets a second room_key message", function(done) {
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
var groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// make the room_key event
var roomKeyEncrypted1 = encryptGroupSessionKey({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// encrypt a message with the group session
var messageEncrypted = encryptMegolmEvent({
senderKey: testSenderKey,
groupSession: groupSession,
room_id: ROOM_ID,
});
// make a second room_key event now that we have advanced the group
// session.
var roomKeyEncrypted2 = encryptGroupSessionKey({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
groupSession: groupSession,
room_id: ROOM_ID,
});
// on the first sync, send the best room key
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, {
next_batch: 1,
to_device: {
events: [roomKeyEncrypted1],
},
});
// on the second sync, send the advanced room key, along with the
// message. This simulates the situation where Alice has been sent a
// later copy of the room key and is reloading the client.
var syncResponse2 = {
next_batch: 2,
to_device: {
events: [roomKeyEncrypted2],
},
rooms: {
join: {},
},
};
syncResponse2.rooms.join[ROOM_ID] = {
timeline: {
events: [messageEncrypted],
},
};
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse2);
return aliceTestClient.httpBackend.flush("/sync", 2).then(function() {
var room = aliceTestClient.client.getRoom(ROOM_ID);
var event = room.getLiveTimeline().getEvents()[0];
expect(event.getContent().body).toEqual('42');
}).nodeify(done);
});
it('Alice sends a megolm message', function(done) {
// establish an olm session with alice
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
var olmEvent = encryptOlmEvent({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
});
var syncResponse = {
next_batch: 1,
to_device: {
events: [olmEvent],
},
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
state: {
events: [
test_utils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
}),
test_utils.mkMembership({
mship: 'join',
sender: '@bob:xyz',
}),
],
},
};
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
var inboundGroupSession;
return aliceTestClient.httpBackend.flush('/sync', 1).then(function() {
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:xyz': {
'DEVICE_ID': testDeviceKeys,
},
}
});
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/m.room.encrypted/'
).respond(200, function(path, content) {
var m = content.messages['@bob:xyz'].DEVICE_ID;
var ct = m.ciphertext[testSenderKey];
var decrypted = JSON.parse(p2pSession.decrypt(ct.type, ct.body));
expect(decrypted.type).toEqual('m.room_key');
inboundGroupSession = new Olm.InboundGroupSession();
inboundGroupSession.create(decrypted.content.session_key);
return {};
});
aliceTestClient.httpBackend.when(
'PUT', '/send/'
).respond(200, function(path, content) {
var ct = content.ciphertext;
var decrypted = JSON.parse(inboundGroupSession.decrypt(ct));
console.log('Decrypted received megolm message', decrypted);
expect(decrypted.type).toEqual('m.room.message');
expect(decrypted.content.body).toEqual('test');
return {
event_id: '$event_id',
};
});
return q.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
]);
}).nodeify(done);
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function(done) {
var syncResponse = {
next_batch: 1,
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
state: {
events: [
test_utils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
}),
test_utils.mkMembership({
mship: 'join',
sender: '@bob:xyz',
}),
],
},
};
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1).then(function() {
console.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:xyz': {},
}
});
return q.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
console.log("Telling alice to send a megolm message");
aliceTestClient.httpBackend.when(
'PUT', '/send/'
).respond(200, {
event_id: '$event_id',
});
return q.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
]);
}).nodeify(done);
});
it("We shouldn't attempt to send to blocked devices", function(done) {
// establish an olm session with alice
var p2pSession = createOlmSession(testOlmAccount, aliceTestClient);
var olmEvent = encryptOlmEvent({
senderKey: testSenderKey,
recipient: aliceTestClient,
p2pSession: p2pSession,
});
var syncResponse = {
next_batch: 1,
to_device: {
events: [olmEvent],
},
rooms: {
join: {},
},
};
syncResponse.rooms.join[ROOM_ID] = {
state: {
events: [
test_utils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
}),
test_utils.mkMembership({
mship: 'join',
sender: '@bob:xyz',
}),
],
},
};
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.httpBackend.flush('/sync', 1).then(function() {
console.log('Forcing alice to download our device keys');
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:xyz': {
'DEVICE_ID': testDeviceKeys,
},
}
});
return q.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
console.log('Telling alice to block our device');
aliceTestClient.client.setDeviceBlocked('@bob:xyz', 'DEVICE_ID');
console.log('Telling alice to send a megolm message');
aliceTestClient.httpBackend.when(
'PUT', '/send/'
).respond(200, {
event_id: '$event_id',
});
return q.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush(),
]);
}).nodeify(done);
});
});
+58 -11
View File
@@ -11,18 +11,17 @@ function HttpBackend() {
var self = this;
// the request function dependency that the SDK needs.
this.requestFn = function(opts, callback) {
var realReq = new Request(opts.method, opts.uri, opts.body, opts.qs);
realReq.callback = callback;
console.log("HTTP backend received request: %s %s", opts.method, opts.uri);
self.requests.push(realReq);
var req = new Request(opts, callback);
console.log("HTTP backend received request: %s", req);
self.requests.push(req);
var abort = function() {
var idx = self.requests.indexOf(realReq);
var idx = self.requests.indexOf(req);
if (idx >= 0) {
console.log("Aborting HTTP request: %s %s", opts.method,
opts.uri);
self.requests.splice(idx, 1);
realReq.callback("aborted");
req.callback("aborted");
}
};
@@ -161,22 +160,32 @@ HttpBackend.prototype = {
* @return {Request} An expected request.
*/
when: function(method, path, data) {
var pendingReq = new Request(method, path, data);
var pendingReq = new ExpectedRequest(method, path, data);
this.expectedRequests.push(pendingReq);
return pendingReq;
}
};
function Request(method, path, data, queryParams) {
/**
* Represents the expectation of a request.
*
* <p>Includes the conditions to be matched against, the checks to be made,
* and the response to be returned.
*
* @constructor
* @param {string} method
* @param {string} path
* @param {object?} data
*/
function ExpectedRequest(method, path, data) {
this.method = method;
this.path = path;
this.data = data;
this.queryParams = queryParams;
this.callback = null;
this.response = null;
this.checks = [];
}
Request.prototype = {
ExpectedRequest.prototype = {
/**
* Execute a check when this request has been satisfied.
* @param {Function} fn The function to execute.
@@ -221,6 +230,44 @@ Request.prototype = {
}
};
/**
* Represents a request made by the app.
*
* @constructor
* @param {object} opts opts passed to request()
* @param {function} callback
*/
function Request(opts, callback) {
this.opts = opts;
this.callback = callback;
Object.defineProperty(this, 'method', {
get: function() { return opts.method; }
});
Object.defineProperty(this, 'path', {
get: function() { return opts.uri; }
});
Object.defineProperty(this, 'data', {
get: function() { return opts.body; }
});
Object.defineProperty(this, 'queryParams', {
get: function() { return opts.qs; }
});
Object.defineProperty(this, 'headers', {
get: function() { return opts.headers || {}; }
});
}
Request.prototype = {
toString: function() {
return this.method + " " + this.path;
},
};
/**
* The HttpBackend class.
*/
+40 -10
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,
sender: opts.user,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
event_id: "$" + Math.random() + "-" + Math.random()
};
if (opts.skey) {
if (opts.skey !== undefined) {
event.state_key = opts.skey;
}
else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
@@ -88,7 +88,7 @@ module.exports.mkPresence = function(opts) {
var event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
sender: opts.user,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: {
avatar_url: opts.url,
displayname: opts.name,
@@ -104,8 +104,8 @@ module.exports.mkPresence = function(opts) {
* @param {Object} opts Values for the membership.
* @param {string} opts.room The room ID for the event.
* @param {string} opts.mship The content.membership for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.skey The other user ID for the event if applicable
* @param {string} opts.sender The sender user ID for the event.
* @param {string} opts.skey The target user ID for the event if applicable
* e.g. for invites/bans.
* @param {string} opts.name The content.displayname for the event.
* @param {string} opts.url The content.avatar_url for the event.
@@ -115,7 +115,7 @@ module.exports.mkPresence = function(opts) {
module.exports.mkMembership = function(opts) {
opts.type = "m.room.member";
if (!opts.skey) {
opts.skey = opts.user;
opts.skey = opts.sender || opts.user;
}
if (!opts.mship) {
throw new Error("Missing .mship => " + JSON.stringify(opts));
@@ -159,7 +159,16 @@ module.exports.mkMessage = function(opts) {
* <p>This is useful for use with integration tests which use asyncronous
* methods: it can be added as a 'catch' handler in a promise chain.
*
* @param {Error} error exception to be reported
* @param {Error} err exception to be reported
*
* @deprecated
* It turns out there are easier ways of doing this. Just use nodeify():
*
* it("should not throw", function(done) {
* asynchronousMethod().then(function() {
* // some tests
* }).nodeify(done);
* });
*
* @example
* it("should not throw", function(done) {
@@ -168,6 +177,27 @@ module.exports.mkMessage = function(opts) {
* }).catch(utils.failTest).done(done);
* });
*/
module.exports.failTest = function(error) {
expect(error.stack).toBe(null);
module.exports.failTest = function(err) {
expect(true).toBe(false, "Testfunc threw: " + err.stack);
};
/**
* A mock implementation of webstorage
*
* @constructor
*/
module.exports.MockStorageApi = function() {
this.data = {};
};
module.exports.MockStorageApi.prototype = {
setItem: function(k, v) {
this.data[k] = v;
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
}
};
+14
View File
@@ -0,0 +1,14 @@
"use strict";
var Crypto = require("../../lib/crypto");
var sdk = require("../..");
describe("Crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
it("Crypto exposes the correct olm library version", function() {
expect(Crypto.getOlmVersion()).toEqual([1, 3, 0]);
});
});
+6 -1
View File
@@ -16,7 +16,12 @@ describe("EventTimeline", function() {
beforeEach(function() {
utils.beforeEach(this);
timeline = new EventTimeline(roomId);
// XXX: this is a horrid hack; should use sinon or something instead to mock
var timelineSet = { room: { roomId: roomId }};
timelineSet.room.getUnfilteredTimelineSet = function() { return timelineSet; };
timeline = new EventTimeline(timelineSet);
});
describe("construction", function() {
+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);
});
});
+68 -6
View File
@@ -51,9 +51,10 @@ describe("MatrixClient", function() {
// }
// items are popped off when processed and block if no items left.
];
var accept_keepalives;
var pendingLookup = null;
function httpReq(cb, method, path, qp, data, prefix) {
if (path === KEEP_ALIVE_PATH) {
if (path === KEEP_ALIVE_PATH && accept_keepalives) {
return q();
}
var next = httpLookups.shift();
@@ -103,6 +104,7 @@ describe("MatrixClient", function() {
if (next.error) {
return q.reject({
errcode: next.error.errcode,
httpStatus: next.error.httpStatus,
name: next.error.errcode,
message: "Expected testing error",
data: next.error
@@ -143,8 +145,10 @@ describe("MatrixClient", function() {
client._http.authedRequest.andCallFake(httpReq);
client._http.authedRequestWithPrefix.andCallFake(httpReq);
client._http.requestWithPrefix.andCallFake(httpReq);
client._http.request.andCallFake(httpReq);
// set reasonable working defaults
accept_keepalives = true;
pendingLookup = null;
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
@@ -204,6 +208,44 @@ describe("MatrixClient", function() {
});
});
describe("getOrCreateFilter", function() {
it("should POST createFilter if no id is present in localStorage", function() {
});
it("should use an existing filter if id is present in localStorage", function() {
});
it("should handle localStorage filterId missing from the server", function(done) {
function getFilterName(userId, suffix) {
// scope this on the user ID because people may login on many accounts
// and they all need to be stored!
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
}
var invalidFilterId = 'invalidF1lt3r';
httpLookups = [];
httpLookups.push({
method: "GET",
path: FILTER_PATH + '/' + invalidFilterId,
error: {
errcode: "M_UNKNOWN",
name: "M_UNKNOWN",
message: "No row found",
data: { errcode: "M_UNKNOWN", error: "No row found" },
httpStatus: 404
}
});
httpLookups.push(FILTER_RESPONSE);
store.getFilterIdByName.andReturn(invalidFilterId);
var filterName = getFilterName(client.credentials.userId);
client.store.setFilterIdByName(filterName, invalidFilterId);
var filter = new sdk.Filter(client.credentials.userId);
client.getOrCreateFilter(filterName, filter).then(function(filterId) {
expect(filterId).toEqual(FILTER_RESPONSE.data.filter_id);
done();
});
});
});
describe("retryImmediately", function() {
it("should return false if there is no request waiting", function() {
client.startClient();
@@ -250,6 +292,8 @@ describe("MatrixClient", function() {
true, "retryImmediately returned false"
);
jasmine.Clock.tick(1);
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
jasmine.Clock.tick(10000);
} else if (state === "SYNCING" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
@@ -329,17 +373,25 @@ describe("MatrixClient", function() {
it("should transition ERROR -> PREPARED after /sync if prev failed",
function(done) {
var expectedStates = [];
accept_keepalives = false;
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, data: {}
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA
});
expectedStates.push(["ERROR", null]);
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["PREPARED", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
@@ -354,14 +406,19 @@ describe("MatrixClient", function() {
});
it("should transition SYNCING -> ERROR after a failed /sync", function(done) {
accept_keepalives = false;
var expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
@@ -394,18 +451,23 @@ describe("MatrixClient", function() {
client.startClient();
});
it("should transition ERROR -> ERROR if multiple /sync fails", function(done) {
it("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
accept_keepalives = false;
var expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
});
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["ERROR", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
+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);
});
});
+31
View File
@@ -102,6 +102,37 @@ describe("RoomMember", function() {
member.setPowerLevelEvent(event); // no-op
expect(emitCount).toEqual(1);
});
it("should honour power levels of zero.",
function() {
var event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@alice:bar": 0,
}
},
event: true
});
var emitCount = 0;
// set the power level to something other than zero or we
// won't get an event
member.powerLevel = 1;
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember.userId).toEqual('@alice:bar');
expect(emitMember.powerLevel).toEqual(0);
expect(emitEvent).toEqual(event);
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(0);
expect(emitCount).toEqual(1);
});
});
describe("setTypingEvent", function() {
+23 -21
View File
@@ -479,14 +479,14 @@ describe("Room", function() {
it("should handle events in the same timeline", function() {
room.addLiveEvents(events);
expect(room.compareEventOrdering(events[0].getId(),
events[1].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
events[1].getId()))
.toBeLessThan(0);
expect(room.compareEventOrdering(events[2].getId(),
events[1].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId(),
events[1].getId()))
.toBeGreaterThan(0);
expect(room.compareEventOrdering(events[1].getId(),
events[1].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
events[1].getId()))
.toEqual(0);
});
@@ -498,11 +498,11 @@ describe("Room", function() {
room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addLiveEvents([events[1]]);
expect(room.compareEventOrdering(events[0].getId(),
events[1].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
events[1].getId()))
.toBeLessThan(0);
expect(room.compareEventOrdering(events[1].getId(),
events[0].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
events[0].getId()))
.toBeGreaterThan(0);
});
@@ -512,24 +512,26 @@ describe("Room", function() {
room.addEventsToTimeline([events[0]], false, oldTimeline);
room.addLiveEvents([events[1]]);
expect(room.compareEventOrdering(events[0].getId(),
events[1].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId(),
events[1].getId()))
.toBe(null);
expect(room.compareEventOrdering(events[1].getId(),
events[0].getId()))
expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId(),
events[0].getId()))
.toBe(null);
});
it("should return null for unknown events", function() {
room.addLiveEvents(events);
expect(room.compareEventOrdering(events[0].getId(), "xxx"))
.toBe(null);
expect(room.compareEventOrdering("xxx", events[0].getId()))
.toBe(null);
expect(room.compareEventOrdering(events[0].getId(),
events[0].getId()))
.toBe(0);
expect(room.getUnfilteredTimelineSet()
.compareEventOrdering(events[0].getId(), "xxx"))
.toBe(null);
expect(room.getUnfilteredTimelineSet()
.compareEventOrdering("xxx", events[0].getId()))
.toBe(null);
expect(room.getUnfilteredTimelineSet()
.compareEventOrdering(events[0].getId(), events[0].getId()))
.toBe(0);
});
});
+18 -14
View File
@@ -18,7 +18,11 @@ function createTimeline(numEvents, baseIndex) {
if (numEvents === undefined) { numEvents = 3; }
if (baseIndex === undefined) { baseIndex = 1; }
var timeline = new EventTimeline(ROOM_ID);
// XXX: this is a horrid hack
var timelineSet = { room: { roomId: ROOM_ID }};
timelineSet.room.getUnfilteredTimelineSet = function() { return timelineSet; };
var timeline = new EventTimeline(timelineSet);
// add the events after the baseIndex first
addEventsToTimeline(timeline, numEvents - baseIndex, false);
@@ -133,19 +137,19 @@ describe("TimelineIndex", function() {
describe("TimelineWindow", function() {
/**
* create a dummy room and client, and a TimelineWindow
* create a dummy eventTimelineSet and client, and a TimelineWindow
* attached to them.
*/
var room, client;
var timelineSet, client;
function createWindow(timeline, opts) {
room = {};
timelineSet = {};
client = {};
client.getEventTimeline = function(room0, eventId0) {
expect(room0).toBe(room);
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineSet0).toBe(timelineSet);
return q(timeline);
};
return new TimelineWindow(client, room, opts);
return new TimelineWindow(client, timelineSet, opts);
}
beforeEach(function() {
@@ -169,15 +173,15 @@ describe("TimelineWindow", function() {
var timeline = createTimeline();
var eventId = timeline.getEvents()[1].getId();
var room = {};
var timelineSet = {};
var client = {};
client.getEventTimeline = function(room0, eventId0) {
expect(room0).toBe(room);
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineSet0).toBe(timelineSet);
expect(eventId0).toEqual(eventId);
return q(timeline);
};
var timelineWindow = new TimelineWindow(client, room);
var timelineWindow = new TimelineWindow(client, timelineSet);
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
@@ -192,12 +196,12 @@ describe("TimelineWindow", function() {
var eventId = timeline.getEvents()[1].getId();
var room = {};
var timelineSet = {};
var client = {};
var timelineWindow = new TimelineWindow(client, room);
var timelineWindow = new TimelineWindow(client, timelineSet);
client.getEventTimeline = function(room0, eventId0) {
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
+60
View File
@@ -200,4 +200,64 @@ describe("utils", function() {
assert.isFalse(utils.deepCompare({ a: { b: func } }, { a: { b: func2 } }));
});
});
describe("extend", function() {
var SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
it("should extend", function() {
var target = {
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
};
var merged = {
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
"newprop": "new",
};
var source_orig = JSON.stringify(SOURCE);
utils.extend(target, SOURCE);
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
// check the originial wasn't modified
expect(JSON.stringify(SOURCE)).toEqual(source_orig);
});
it("should ignore null", function() {
var target = {
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
};
var merged = {
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
"newprop": "new",
};
var source_orig = JSON.stringify(SOURCE);
utils.extend(target, null, SOURCE);
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
// check the originial wasn't modified
expect(JSON.stringify(SOURCE)).toEqual(source_orig);
});
it("should handle properties created with defineProperties", function() {
var source = Object.defineProperties({}, {
"enumerableProp": {
get: function() {
return true;
},
enumerable: true
},
"nonenumerableProp": {
get: function() {
return true;
}
}
});
var target = {};
utils.extend(target, source);
expect(target.enumerableProp).toBe(true);
expect(target.nonenumerableProp).toBe(undefined);
});
});
});
+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;