Compare commits

...

437 Commits

Author SHA1 Message Date
David Baker 070d58ac0e v0.7.6-rc.2 2017-04-10 14:52:10 +01:00
David Baker 2d70f69857 Prepare changelog for v0.7.6-rc.2 2017-04-10 14:52:09 +01:00
David Baker 7a3acfa6a7 Merge remote-tracking branch 'origin/develop' into release-v0.7.6 2017-04-10 14:50:16 +01:00
David Baker 8ac0d12d1e Merge pull request #416 from matrix-org/dbkr/feature_detect_webworker
Add feature detection for webworkers
2017-04-10 11:28:40 +01:00
David Baker 86164103f0 Allow webworker API to be passed in
So it can be used from Node with one of the compatible APIs
2017-04-10 10:02:06 +01:00
David Baker b9c71ef03f Add feature detection for webworkers
Only use web worker store if we have web workers available
2017-04-07 17:45:45 +01:00
David Baker 7ffff761d5 Merge remote-tracking branch 'origin/develop' into release-v0.7.6 2017-04-07 17:01:25 +01:00
Richard van der Hoff 7d4366473d Merge pull request #415 from matrix-org/dbkr/fix_release_script
Fix release script
2017-04-07 17:00:12 +01:00
David Baker e63c660162 Fix release script
Publish to npm before switching to the doc branch: previously we
published from master, but since we now now longer merge
pre-releases to master, publish from the release branch (just
not the doc branch because that won't work).
2017-04-07 16:55:38 +01:00
David Baker 1762f9d68e v0.7.6-rc.1 2017-04-07 16:44:21 +01:00
David Baker 76287eed2c Prepare changelog for v0.7.6-rc.1 2017-04-07 16:44:20 +01:00
David Baker 5a764bbaa2 Merge pull request #414 from matrix-org/dbkr/indexeddb_save_after_first_sync
Make indexeddb save after the first sync
2017-04-07 16:26:03 +01:00
David Baker ce9e69c9e0 Merge remote-tracking branch 'origin/develop' into dbkr/indexeddb_save_after_first_sync 2017-04-07 16:21:54 +01:00
David Baker 3a74e1f154 Merge remote-tracking branch 'origin/dbkr/indexeddb_webworker_dont_transfer_sync' into dbkr/indexeddb_save_after_first_sync 2017-04-07 16:21:21 +01:00
David Baker 6df4a36da9 Merge pull request #413 from matrix-org/dbkr/indexeddb_webworker_dont_transfer_sync
Make indexeddb startup faster
2017-04-07 16:20:44 +01:00
David Baker 4e38b51958 Merge remote-tracking branch 'origin/develop' into dbkr/indexeddb_webworker_dont_transfer_sync 2017-04-07 15:12:54 +01:00
David Baker b6c036af25 Merge pull request #412 from matrix-org/dbkr/indexeddb_webworker
Add ability to do indexeddb sync work in webworker
2017-04-07 15:12:22 +01:00
David Baker dd789a8dcc Merge branch 'dbkr/indexeddb_webworker_dont_transfer_sync' into dbkr/indexeddb_save_after_first_sync 2017-04-07 15:10:17 +01:00
David Baker ca83b858c0 lint 2017-04-07 15:09:42 +01:00
David Baker 0f29952a1c Smush connect() and init() together 2017-04-07 15:06:38 +01:00
David Baker e2d7b465ae Merge branch 'dbkr/indexeddb_webworker' into dbkr/indexeddb_webworker_dont_transfer_sync 2017-04-07 15:01:42 +01:00
David Baker 8985dc2f7e Update import in example 2017-04-07 14:42:26 +01:00
David Baker cf1731792c Add separate import file for indxexeddb worker
And removing it from the main one
2017-04-07 14:41:12 +01:00
David Baker 4c200cdd49 lint 2017-04-07 14:26:43 +01:00
David Baker 5c8eacddde Remove old comment 2017-04-07 12:01:31 +01:00
David Baker 2668177210 Oops, moved the variable 2017-04-07 11:58:16 +01:00
David Baker 62be08f063 Make sure worker setup doesn't race 2017-04-07 11:55:13 +01:00
David Baker ab2a67a012 Don't try to send Error objects 2017-04-07 11:42:18 +01:00
David Baker 3ceeee7298 Typos 2017-04-07 11:40:21 +01:00
David Baker 2d7576f29b Doc usage of the webworker class 2017-04-07 11:37:31 +01:00
David Baker 6e25a17afb Typos 2017-04-07 11:26:52 +01:00
David Baker 0715682a8b Name IndexedDBStoreWorker consistently 2017-04-07 11:22:07 +01:00
David Baker cfff30c314 Use once to clean up listener 2017-04-07 11:10:57 +01:00
David Baker b53318ecb7 Make indexeddb save after the first sync
Save the sync data to indexeddb after the first catch-up

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

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

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

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

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

* Oops, fix merge

* Fix lint

* Add submitMsisdnToken

* Support the bind_msisdn param to register

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fixes https://github.com/vector-im/riot-web/issues/2676.
2016-12-06 17:09:21 +00:00
92 changed files with 10103 additions and 6416 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"presets": ["es2015"]
}
+23
View File
@@ -0,0 +1,23 @@
# Copyright 2017 Aviral Dasgupta
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
root = true
[*]
charset=utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
+67
View File
@@ -0,0 +1,67 @@
module.exports = {
parser: "babel-eslint",
parserOptions: {
ecmaVersion: 6,
sourceType: "module",
ecmaFeatures: {
}
},
env: {
browser: true,
node: true,
},
extends: ["eslint:recommended", "google"],
rules: {
// rules we've always adhered to or now do
"max-len": ["error", {
code: 90,
ignoreComments: true,
}],
curly: ["error", "multi-line"],
"prefer-const": ["error"],
"comma-dangle": ["error", {
arrays: "always-multiline",
objects: "always-multiline",
imports: "always-multiline",
exports: "always-multiline",
functions: "always-multiline",
}],
// loosen jsdoc requirements a little
"require-jsdoc": ["error", {
require: {
FunctionDeclaration: false,
}
}],
"valid-jsdoc": ["error", {
requireParamDescription: false,
requireReturn: false,
requireReturnDescription: false,
}],
// rules we do not want from eslint-recommended
"no-console": ["off"],
"no-constant-condition": ["off"],
"no-empty": ["error", { "allowEmptyCatch": true }],
// rules we do not want from the google styleguide
"object-curly-spacing": ["off"],
"spaced-comment": ["off"],
// in principle we prefer single quotes, but life is too short
quotes: ["off"],
// rules we'd ideally like to adhere to, but the current
// code does not (in most cases because it's still ES5)
// we set these to warnings, and assert that the number
// of warnings doesn't exceed a given threshold
"no-var": ["warn"],
"brace-style": ["warn", "1tbs", {"allowSingleLine": true}],
"prefer-rest-params": ["warn"],
"prefer-spread": ["warn"],
"one-var": ["warn"],
"padded-blocks": ["warn"],
"no-extend-native": ["warn"],
"camelcase": ["warn"],
}
}
+2
View File
@@ -7,6 +7,8 @@ lib-cov
out
reports
/dist
/lib
/specbuild
# version file and tarball created by 'npm pack'
/git-revision.txt
-11
View File
@@ -1,11 +0,0 @@
{
"node": true,
"jasmine": true,
"nonew": true,
"curly": true,
"forin": true,
"freeze": false,
"undef": true,
"unused": "vars"
}
+3
View File
@@ -1,3 +1,6 @@
language: node_js
node_js:
- node # Latest stable version of nodejs.
script:
- npm run lint
- npm run test
+227
View File
@@ -1,3 +1,230 @@
Changes in [0.7.6-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.6-rc.2) (2017-04-10)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.6-rc.1...v0.7.6-rc.2)
* Add feature detection for webworkers
[\#416](https://github.com/matrix-org/matrix-js-sdk/pull/416)
* Fix release script
[\#415](https://github.com/matrix-org/matrix-js-sdk/pull/415)
Changes in [0.7.6-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.6-rc.1) (2017-04-07)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.5...v0.7.6-rc.1)
* Make indexeddb save after the first sync
[\#414](https://github.com/matrix-org/matrix-js-sdk/pull/414)
* Make indexeddb startup faster
[\#413](https://github.com/matrix-org/matrix-js-sdk/pull/413)
* Add ability to do indexeddb sync work in webworker
[\#412](https://github.com/matrix-org/matrix-js-sdk/pull/412)
* Move more functionality to the indexeddb backend
[\#409](https://github.com/matrix-org/matrix-js-sdk/pull/409)
* Indicate syncState ERROR after many failed /syncs
[\#410](https://github.com/matrix-org/matrix-js-sdk/pull/410)
* Further reorganising of indexeddb sync code
[\#407](https://github.com/matrix-org/matrix-js-sdk/pull/407)
* Change interface of IndexedDBStore: hide internals
[\#406](https://github.com/matrix-org/matrix-js-sdk/pull/406)
* Don't be SYNCING until updating from the server
[\#405](https://github.com/matrix-org/matrix-js-sdk/pull/405)
* Don't log the entire /sync response
[\#403](https://github.com/matrix-org/matrix-js-sdk/pull/403)
* webrtc/call: Assign MediaStream to video element srcObject
[\#402](https://github.com/matrix-org/matrix-js-sdk/pull/402)
* Fix undefined reference in http-api
[\#400](https://github.com/matrix-org/matrix-js-sdk/pull/400)
* Add copyright header to event-timeline.js
[\#382](https://github.com/matrix-org/matrix-js-sdk/pull/382)
* client: fix docs for user-scoped account_data events
[\#397](https://github.com/matrix-org/matrix-js-sdk/pull/397)
* Add a CONTRIBUTING for js-sdk
[\#399](https://github.com/matrix-org/matrix-js-sdk/pull/399)
* Fix leaking room state objects on limited sync responses
[\#395](https://github.com/matrix-org/matrix-js-sdk/pull/395)
* Extend 'ignoreFailure' to be 'background'
[\#396](https://github.com/matrix-org/matrix-js-sdk/pull/396)
* Add x_show_msisdn parameter to register calls
[\#388](https://github.com/matrix-org/matrix-js-sdk/pull/388)
* Update event redaction to keep sender and origin_server_ts
[\#394](https://github.com/matrix-org/matrix-js-sdk/pull/394)
* Handle 'limited' timeline responses in the SyncAccumulator
[\#393](https://github.com/matrix-org/matrix-js-sdk/pull/393)
* Give a better error message if the HS doesn't support msisdn registeration
[\#391](https://github.com/matrix-org/matrix-js-sdk/pull/391)
* Add getEmailSid
[\#383](https://github.com/matrix-org/matrix-js-sdk/pull/383)
* Add m.login.email.identity support to UI auth
[\#380](https://github.com/matrix-org/matrix-js-sdk/pull/380)
* src/client.js: Fix incorrect roomId reference in VoIP glare code
[\#381](https://github.com/matrix-org/matrix-js-sdk/pull/381)
* add .editorconfig
[\#379](https://github.com/matrix-org/matrix-js-sdk/pull/379)
* Store account data in the same way as room data
[\#377](https://github.com/matrix-org/matrix-js-sdk/pull/377)
* Upload one-time keys on /sync rather than a timer
[\#376](https://github.com/matrix-org/matrix-js-sdk/pull/376)
* Increase the WRITE_DELAY on database syncing
[\#374](https://github.com/matrix-org/matrix-js-sdk/pull/374)
* Make deleteAllData() return a Promise
[\#373](https://github.com/matrix-org/matrix-js-sdk/pull/373)
* Don't include banned users in the room name
[\#372](https://github.com/matrix-org/matrix-js-sdk/pull/372)
* Support IndexedDB as a backing store
[\#363](https://github.com/matrix-org/matrix-js-sdk/pull/363)
* Poll /sync with a short timeout while catching up
[\#370](https://github.com/matrix-org/matrix-js-sdk/pull/370)
* Make test coverage work again
[\#368](https://github.com/matrix-org/matrix-js-sdk/pull/368)
* Add docs to event
[\#367](https://github.com/matrix-org/matrix-js-sdk/pull/367)
* Keep the device-sync token more up-to-date
[\#366](https://github.com/matrix-org/matrix-js-sdk/pull/366)
* Fix race conditions in device list download
[\#365](https://github.com/matrix-org/matrix-js-sdk/pull/365)
* Fix the unban method
[\#364](https://github.com/matrix-org/matrix-js-sdk/pull/364)
* Spread out device verification work
[\#362](https://github.com/matrix-org/matrix-js-sdk/pull/362)
* Clean up/improve e2e logging
[\#361](https://github.com/matrix-org/matrix-js-sdk/pull/361)
* Fix decryption of events whose key arrives later
[\#360](https://github.com/matrix-org/matrix-js-sdk/pull/360)
* Invalidate device lists when encryption is enabled in a room
[\#359](https://github.com/matrix-org/matrix-js-sdk/pull/359)
* Switch from jasmine to mocha + expect + lolex
[\#358](https://github.com/matrix-org/matrix-js-sdk/pull/358)
* Install source-map-support in each test
[\#356](https://github.com/matrix-org/matrix-js-sdk/pull/356)
* searchMessageText: avoid setting keys=undefined
[\#357](https://github.com/matrix-org/matrix-js-sdk/pull/357)
* realtime-callbacks: pass `global` as `this`
[\#355](https://github.com/matrix-org/matrix-js-sdk/pull/355)
* Make the tests work without olm
[\#354](https://github.com/matrix-org/matrix-js-sdk/pull/354)
* Tests: Factor out TestClient and use it in crypto tests
[\#353](https://github.com/matrix-org/matrix-js-sdk/pull/353)
* Fix some lint
[\#352](https://github.com/matrix-org/matrix-js-sdk/pull/352)
* Make a sig for source tarballs when releasing
[\#351](https://github.com/matrix-org/matrix-js-sdk/pull/351)
* When doing a pre-release, don't bother merging to master and develop.
[\#350](https://github.com/matrix-org/matrix-js-sdk/pull/350)
Changes in [0.7.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.5) (2017-02-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.5-rc.3...v0.7.5)
No changes from 0.7.5-rc.3
Changes in [0.7.5-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.5-rc.3) (2017-02-03)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.5-rc.2...v0.7.5-rc.3)
* Include DeviceInfo in deviceVerificationChanged events
[a3cc8eb](https://github.com/matrix-org/matrix-js-sdk/commit/a3cc8eb1f6d165576a342596f638316721cb26b6)
* Fix device list update
[5fd7410](https://github.com/matrix-org/matrix-js-sdk/commit/5fd74109ffc56b73deb40c2604d84c38b8032c40)
Changes in [0.7.5-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.5-rc.2) (2017-02-03)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.5-rc.1...v0.7.5-rc.2)
* Use the device change notifications interface
[\#348](https://github.com/matrix-org/matrix-js-sdk/pull/348)
* Rewrite the device key query logic
[\#347](https://github.com/matrix-org/matrix-js-sdk/pull/347)
Changes in [0.7.5-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.5-rc.1) (2017-02-03)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.4...v0.7.5-rc.1)
* Support for blacklisting unverified devices, both per-room and globally
[\#336](https://github.com/matrix-org/matrix-js-sdk/pull/336)
* track errors when events can't be sent
[\#349](https://github.com/matrix-org/matrix-js-sdk/pull/349)
* Factor out device list management
[\#346](https://github.com/matrix-org/matrix-js-sdk/pull/346)
* Support for warning users when unknown devices show up
[\#335](https://github.com/matrix-org/matrix-js-sdk/pull/335)
* Enable sourcemaps in browserified distro
[\#345](https://github.com/matrix-org/matrix-js-sdk/pull/345)
* Record all e2e room settings in localstorage
[\#344](https://github.com/matrix-org/matrix-js-sdk/pull/344)
* Make Olm work with browserified js-sdk
[\#340](https://github.com/matrix-org/matrix-js-sdk/pull/340)
* Make browserify a dev dependency
[\#339](https://github.com/matrix-org/matrix-js-sdk/pull/339)
* Allow single line brace-style
[\#338](https://github.com/matrix-org/matrix-js-sdk/pull/338)
* Turn on comma-dangle for function calls
[\#333](https://github.com/matrix-org/matrix-js-sdk/pull/333)
* Add prefer-const
[\#331](https://github.com/matrix-org/matrix-js-sdk/pull/331)
* Support for importing and exporting megolm sessions
[\#326](https://github.com/matrix-org/matrix-js-sdk/pull/326)
* Fix linting on all tests
[\#329](https://github.com/matrix-org/matrix-js-sdk/pull/329)
* Fix ESLint warnings and errors
[\#325](https://github.com/matrix-org/matrix-js-sdk/pull/325)
* BREAKING CHANGE: Remove WebStorageStore
[\#324](https://github.com/matrix-org/matrix-js-sdk/pull/324)
Changes in [0.7.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.4) (2017-01-16)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.4-rc.1...v0.7.4)
* Fix non-conference calling
Changes in [0.7.4-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.4-rc.1) (2017-01-13)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.3...v0.7.4-rc.1)
* Remove babel-polyfill
[\#321](https://github.com/matrix-org/matrix-js-sdk/pull/321)
* Update build process for ES6
[\#320](https://github.com/matrix-org/matrix-js-sdk/pull/320)
* 'babel' is not a babel package anymore
[\#319](https://github.com/matrix-org/matrix-js-sdk/pull/319)
* Add Babel for ES6 support
[\#318](https://github.com/matrix-org/matrix-js-sdk/pull/318)
* Move screen sharing check/error
[\#317](https://github.com/matrix-org/matrix-js-sdk/pull/317)
* release.sh: Bail early if there are uncommitted changes
[\#316](https://github.com/matrix-org/matrix-js-sdk/pull/316)
Changes in [0.7.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.3) (2017-01-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.2...v0.7.3)
* User presence list feature
[\#310](https://github.com/matrix-org/matrix-js-sdk/pull/310)
* Allow clients the ability to set a default local timeout
[\#313](https://github.com/matrix-org/matrix-js-sdk/pull/313)
* Add API to delete threepid
[\#312](https://github.com/matrix-org/matrix-js-sdk/pull/312)
Changes in [0.7.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.2) (2016-12-15)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.1...v0.7.2)
* Bump to Olm 2.0
[\#309](https://github.com/matrix-org/matrix-js-sdk/pull/309)
* Sanity check payload length before encrypting
[\#307](https://github.com/matrix-org/matrix-js-sdk/pull/307)
* Remove dead _sendPingToDevice function
[\#308](https://github.com/matrix-org/matrix-js-sdk/pull/308)
* Add setRoomDirectoryVisibilityAppService
[\#306](https://github.com/matrix-org/matrix-js-sdk/pull/306)
* Update release script to do signed releases
[\#305](https://github.com/matrix-org/matrix-js-sdk/pull/305)
* e2e: Wait for pending device lists
[\#304](https://github.com/matrix-org/matrix-js-sdk/pull/304)
* Start a new megolm session when devices are blacklisted
[\#303](https://github.com/matrix-org/matrix-js-sdk/pull/303)
* E2E: Download our own devicelist on startup
[\#302](https://github.com/matrix-org/matrix-js-sdk/pull/302)
Changes in [0.7.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.1) (2016-12-09)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.1-rc.1...v0.7.1)
+112 -1
View File
@@ -1,4 +1,115 @@
Contributing code to matrix-js-sdk
==================================
matrix-js-sdk follows the same pattern as https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst
Everyone is welcome to contribute code to matrix-js-sdk, provided that they are
willing to license their contributions under the same license as the project
itself. We follow a simple 'inbound=outbound' model for contributions: the act
of submitting an 'inbound' contribution means that the contributor agrees to
license the code under the same terms as the project's overall 'outbound'
license - in this case, Apache Software License v2 (see `<LICENSE>`_).
How to contribute
~~~~~~~~~~~~~~~~~
The preferred and easiest way to contribute changes to the project is to fork
it on github, and then create a pull request to ask us to pull your changes
into our repo (https://help.github.com/articles/using-pull-requests/)
**The single biggest thing you need to know is: please base your changes on
the develop branch - /not/ master.**
We use the master branch to track the most recent release, so that folks who
blindly clone the repo and automatically check out master get something that
works. Develop is the unstable branch where all the development actually
happens: the workflow is that contributors should fork the develop branch to
make a 'feature' branch for a particular contribution, and then make a pull
request to merge this back into the matrix.org 'official' develop branch. We
use github's pull request workflow to review the contribution, and either ask
you to make any refinements needed or merge it and make them ourselves. The
changes will then land on master when we next do a release.
We use Travis for continuous integration, and all pull requests get
automatically tested by Travis: if your change breaks the build, then the PR
will show that there are failed checks, so please check back after a few
minutes.
Code style
~~~~~~~~~~
The code-style for matrix-js-sdk is not formally documented, but contributors
are encouraged to read the code style document for matrix-react-sdk
(`<https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md>`_)
and follow the principles set out there.
Please ensure your changes match the cosmetic style of the existing project,
and **never** mix cosmetic and functional changes in the same commit, as it
makes it horribly hard to review otherwise.
Attribution
~~~~~~~~~~~
Everyone who contributes anything to Matrix is welcome to be listed in the
AUTHORS.rst file for the project in question. Please feel free to include a
change to AUTHORS.rst in your pull request to list yourself and a short
description of the area(s) you've worked on. Also, we sometimes have swag to
give away to contributors - if you feel that Matrix-branded apparel is missing
from your life, please mail us your shipping address to matrix at matrix.org
and we'll try to fix it :)
Sign off
~~~~~~~~
In order to have a concrete record that your contribution is intentional
and you agree to license it under the same terms as the project's license, we've adopted the
same lightweight approach that the Linux Kernel
(https://www.kernel.org/doc/Documentation/SubmittingPatches), Docker
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
projects use: the DCO (Developer Certificate of Origin:
http://developercertificate.org/). This is a simple declaration that you wrote
the contribution or otherwise have the right to contribute it to Matrix::
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
If you agree to this for your contribution, then all that's needed is to
include the line in your commit or pull request comment::
Signed-off-by: Your Name <your@email.example.org>
...using your real name; unfortunately pseudonyms and anonymous contributions
can't be accepted. Git makes this trivial - just use the -s flag when you do
``git commit``, having first set ``user.name`` and ``user.email`` git configs
(which you should have done anyway :)
+1
View File
@@ -65,6 +65,7 @@ Later versions of the SDK will:
Usage
=====
Conventions
-----------
-14
View File
@@ -1,14 +0,0 @@
There is a script `release.sh` which does the following, but if you need to do
a release manually, here are the steps:
- `git checkout -b release-v0.x.x`
- Update `CHANGELOG.md`
- `npm version 0.x.x`
- Merge `release-v0.x.x` onto `master`.
- Push `master`.
- Push the tag: `git push --tags`
- `npm publish`
- Generate documentation: `npm run gendoc` (this outputs HTML to `.jsdoc`)
- Copy the documentation from `.jsdoc` to the `gh-pages` branch and update `index.html`
- Merge `master` onto `develop`.
- Push `develop`.
+31
View File
@@ -0,0 +1,31 @@
Random notes from Matthew on the two possible approaches for warning users about unexpected
unverified devices popping up in their rooms....
Original idea...
================
Warn when an existing user adds an unknown device to a room.
Warn when a user joins the room with unverified or unknown devices.
Warn when you initial sync if the room has any unverified devices in it.
^ this is good enough if we're doing local storage.
OR, better:
Warn when you initial sync if the room has any new undefined devices since you were last there.
=> This means persisting the rooms that devices are in, across initial syncs.
Updated idea...
===============
Warn when the user tries to send a message:
- If the room has unverified devices which the user has not yet been told about in the context of this room
...or in the context of this user? currently all verification is per-user, not per-room.
...this should be good enough.
- so track whether we have warned the user or not about unverified devices - blocked, unverified, verified, unverified_warned.
throw an error when trying to encrypt if there are pure unverified devices there
app will have to search for the devices which are pure unverified to warn about them - have to do this from MembersList anyway?
- or megolm could warn which devices are causing the problems.
Why do we wait to establish outbound sessions? It just makes a horrible pause when we first try to send a message... but could otherwise unnecessarily consume resources?
+1 -1
View File
@@ -44,7 +44,7 @@ window.onload = function() {
disableButtons(true, true, true);
};
matrixClient.on("sync", function(state, prevState, data) {
client.on("sync", function(state, prevState, data) {
switch (state) {
case "PREPARED":
syncComplete();
+11 -10
View File
@@ -1,9 +1,12 @@
#!/bin/bash -l
export NVM_DIR="/home/jenkins/.nvm"
set -x
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 0.10
npm install
nvm use 6 || exit $?
npm install || exit $?
RC=0
@@ -12,15 +15,13 @@ function fail {
RC=1
}
# don't use last time's test reports
rm -rf reports coverage || exit $?
npm test || fail "npm test finished with return code $?"
jshint --reporter=checkstyle -c .jshint lib spec > jshint.xml ||
fail "jshint finished with return code $?"
gjslint --unix_mode --disable 0131,0211,0200,0222,0212 \
--max_line_length 90 \
-r lib/ -r spec/ > gjslint.log ||
fail "gjslint finished with return code $?"
npm run -s lint -- -f checkstyle > eslint.xml ||
fail "eslint finished with return code $?"
# delete the old tarball, if it exists
rm -f matrix-js-sdk-*.tgz
-228
View File
@@ -1,228 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/** @module 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;
-686
View File
@@ -1,686 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module. Implementation details:
* <pre>
* Room data is stored as follows:
* room_$ROOMID_timeline_$INDEX : [ Event, Event, Event ]
* room_$ROOMID_state : {
* pagination_token: <oldState.paginationToken>,
* events: {
* <event_type>: { <state_key> : {JSON} }
* }
* }
* User data is stored as follows:
* user_$USERID : User
* Sync token:
* sync_token : $TOKEN
*
* Room Retrieval
* --------------
* Retrieving a room requires the $ROOMID which then pulls out the current state
* from room_$ROOMID_state. A defined starting batch of timeline events are then
* extracted from the highest numbered $INDEX for room_$ROOMID_timeline_$INDEX
* (more indices as required). The $INDEX may be negative. These are
* added to the timeline in the same way as /initialSync (old state will diverge).
* If there exists a room_$ROOMID_timeline_live key, then a timeline sync should
* be performed before retrieving.
*
* Retrieval of earlier messages
* -----------------------------
* The earliest event the Room instance knows about is E. Retrieving earlier
* messages requires a Room which has a storageToken defined.
* This token maps to the index I where the Room is at. Events are then retrieved from
* room_$ROOMID_timeline_{I} and elements before E are extracted. If the limit
* demands more events, I-1 is retrieved, up until I=min $INDEX where it gives
* less than the limit. Index may go negative if you have paginated in the past.
*
* Full Insertion
* --------------
* Storing a room requires the timeline and state keys for $ROOMID to
* be blown away and completely replaced, which is computationally expensive.
* Room.timeline is batched according to the given batch size B. These batches
* are then inserted into storage as room_$ROOMID_timeline_$INDEX. Finally,
* the current room state is persisted to room_$ROOMID_state.
*
* Incremental Insertion
* ---------------------
* As events arrive, the store can quickly persist these new events. This
* involves pushing the events to room_$ROOMID_timeline_live. If the
* current room state has been modified by the new event, then
* room_$ROOMID_state should be updated in addition to the timeline.
*
* Timeline sync
* -------------
* Retrieval of events from the timeline depends on the proper batching of
* events. This is computationally expensive to perform on every new event, so
* is deferred by inserting live events to room_$ROOMID_timeline_live. A
* timeline sync reconciles timeline_live and timeline_$INDEX. This involves
* retrieving _live and the highest numbered $INDEX batch. If the batch is < B,
* the earliest entries from _live are inserted into the $INDEX until the
* batch == B. Then, the remaining entries in _live are batched to $INDEX+1,
* $INDEX+2, and so on. The easiest way to visualise this is that the timeline
* goes from old to new, left to right:
* -2 -1 0 1
* <--OLD---------------------------------------NEW-->
* [a,b,c] [d,e,f] [g,h,i] [j,k,l]
*
* Purging
* -------
* Events from the timeline can be purged by removing the lowest
* timeline_$INDEX in the store.
*
* Example
* -------
* A room with room_id !foo:bar has 9 messages (M1->9 where 9=newest) with a
* batch size of 4. The very first time, there is no entry for !foo:bar until
* storeRoom() is called, which results in the keys: [Full Insert]
* room_!foo:bar_timeline_0 : [M1, M2, M3, M4]
* room_!foo:bar_timeline_1 : [M5, M6, M7, M8]
* room_!foo:bar_timeline_2 : [M9]
* room_!foo:bar_state: { ... }
*
* 5 new messages (N1-5, 5=newest) arrive and are then added: [Incremental Insert]
* room_!foo:bar_timeline_live: [N1]
* room_!foo:bar_timeline_live: [N1, N2]
* room_!foo:bar_timeline_live: [N1, N2, N3]
* room_!foo:bar_timeline_live: [N1, N2, N3, N4]
* room_!foo:bar_timeline_live: [N1, N2, N3, N4, N5]
*
* App is shutdown. Restarts. The timeline is synced [Timeline Sync]
* room_!foo:bar_timeline_2 : [M9, N1, N2, N3]
* room_!foo:bar_timeline_3 : [N4, N5]
* room_!foo:bar_timeline_live: []
*
* And the room is retrieved with 8 messages: [Room Retrieval]
* Room.timeline: [M7, M8, M9, N1, N2, N3, N4, N5]
* Room.storageToken: => early_index = 1 because that's where M7 is.
*
* 3 earlier messages are requested: [Earlier retrieval]
* Use storageToken to find batch index 1. Scan batch for earliest event ID.
* earliest event = M7
* events = room_!foo:bar_timeline_1 where event < M7 = [M5, M6]
* Too few events, use next index (0) and get 1 more:
* events = room_!foo:bar_timeline_0 = [M1, M2, M3, M4] => [M4]
* Return concatentation:
* [M4, M5, M6]
*
* Purge oldest events: [Purge]
* del room_!foo:bar_timeline_0
* </pre>
* @module store/webstorage
*/
var DEBUG = false; // set true to enable console logging.
var utils = require("../utils");
var Room = require("../models/room");
var User = require("../models/user");
var MatrixEvent = require("../models/event").MatrixEvent;
/**
* Construct a web storage store, capable of storing rooms and users.
* @constructor
* @param {WebStorage} webStore A web storage implementation, e.g.
* 'window.localStorage' or 'window.sessionStorage' or a custom implementation.
* @param {integer} batchSize The number of events to store per key/value (room
* scoped). Use -1 to store all events for a room under one key/value.
* @throws if the supplied 'store' does not meet the Storage interface of the
* WebStorage API.
*/
function WebStorageStore(webStore, batchSize) {
this.store = webStore;
this.batchSize = batchSize;
if (!utils.isFunction(webStore.getItem) || !utils.isFunction(webStore.setItem) ||
!utils.isFunction(webStore.removeItem) || !utils.isFunction(webStore.key)) {
throw new Error(
"Supplied webStore does not meet the WebStorage API interface"
);
}
if (!parseInt(webStore.length) && webStore.length !== 0) {
throw new Error(
"Supplied webStore does not meet the WebStorage API interface (length)"
);
}
// cached list of room_ids this is storing.
this._roomIds = [];
this._syncedWithStore = false;
// tokens used to remember which index the room instance is at.
this._tokens = [
// { earliestIndex: -4 }
];
}
/**
* Retrieve the token to stream from.
* @return {string} The token or null.
*/
WebStorageStore.prototype.getSyncToken = function() {
return this.store.getItem("sync_token");
};
/**
* Set the token to stream from.
* @param {string} token The token to stream from.
*/
WebStorageStore.prototype.setSyncToken = function(token) {
this.store.setItem("sync_token", token);
};
/**
* Store a room in web storage.
* @param {Room} room
*/
WebStorageStore.prototype.storeRoom = function(room) {
var serRoom = SerialisedRoom.fromRoom(room, this.batchSize);
persist(this.store, serRoom);
if (this._roomIds.indexOf(room.roomId) === -1) {
this._roomIds.push(room.roomId);
}
};
/**
* Retrieve a room from web storage.
* @param {string} roomId
* @return {?Room}
*/
WebStorageStore.prototype.getRoom = function(roomId) {
// probe if room exists; break early if not. Every room should have state.
if (!getItem(this.store, keyName(roomId, "state"))) {
debuglog("getRoom: No room with id %s found.", roomId);
return null;
}
var timelineKeys = getTimelineIndices(this.store, roomId);
if (timelineKeys.indexOf("live") !== -1) {
debuglog("getRoom: Live events found. Syncing timeline for %s", roomId);
this._syncTimeline(roomId, timelineKeys);
}
return loadRoom(this.store, roomId, this.batchSize, this._tokens);
};
/**
* Get a list of all rooms from web storage.
* @return {Array} An empty array.
*/
WebStorageStore.prototype.getRooms = function() {
var rooms = [];
var i;
if (!this._syncedWithStore) {
// sync with the store to set this._roomIds correctly. We know there is
// exactly one 'state' key for each room, so we grab them.
this._roomIds = [];
for (i = 0; i < this.store.length; i++) {
if (this.store.key(i).indexOf("room_") === 0 &&
this.store.key(i).indexOf("_state") !== -1) {
// grab the middle bit which is the room ID
var k = this.store.key(i);
this._roomIds.push(
k.substring("room_".length, k.length - "_state".length)
);
}
}
this._syncedWithStore = true;
}
// call getRoom on each room_id
for (i = 0; i < this._roomIds.length; i++) {
var rm = this.getRoom(this._roomIds[i]);
if (rm) {
rooms.push(rm);
}
}
return rooms;
};
/**
* Get a list of summaries from web storage.
* @return {Array} An empty array.
*/
WebStorageStore.prototype.getRoomSummaries = function() {
return [];
};
/**
* Store a user in web storage.
* @param {User} user
*/
WebStorageStore.prototype.storeUser = function(user) {
// persist the events used to make the user, we can reconstruct on demand.
setItem(this.store, "user_" + user.userId, {
presence: user.events.presence ? user.events.presence.event : null
});
};
/**
* Get a user from web storage.
* @param {string} userId
* @return {User}
*/
WebStorageStore.prototype.getUser = function(userId) {
var userData = getItem(this.store, "user_" + userId);
if (!userData) {
return null;
}
var user = new User(userId);
if (userData.presence) {
user.setPresenceEvent(new MatrixEvent(userData.presence));
}
return user;
};
/**
* Retrieve scrollback for this room. Automatically adds events to the timeline.
* @param {Room} room The matrix room to add the events to the start of the timeline.
* @param {integer} limit The max number of old events to retrieve.
* @return {Array<Object>} An array of objects which will be at most 'limit'
* length and at least 0. The objects are the raw event JSON. The last element
* is the 'oldest' (for parity with homeserver scrollback APIs).
*/
WebStorageStore.prototype.scrollback = function(room, limit) {
if (room.storageToken === undefined || room.storageToken >= this._tokens.length) {
return [];
}
// find the index of the earliest event in this room's timeline
var storeData = this._tokens[room.storageToken] || {};
var i;
var earliestIndex = storeData.earliestIndex;
var earliestEventId = room.timeline[0] ? room.timeline[0].getId() : null;
debuglog(
"scrollback in %s (timeline=%s msgs) i=%s, timeline[0].id=%s - req %s events",
room.roomId, room.timeline.length, earliestIndex, earliestEventId, limit
);
var batch = getItem(
this.store, keyName(room.roomId, "timeline", earliestIndex)
);
if (!batch) {
// bad room or already at start, either way we have nothing to give.
debuglog("No batch with index %s found.", earliestIndex);
return [];
}
// populate from this batch first
var scrollback = [];
var foundEventId = false;
for (i = batch.length - 1; i >= 0; i--) {
// go back and find the earliest event ID, THEN start adding entries.
// Make a MatrixEvent so we don't assume .event_id exists
// (e.g v2/v3 JSON may be different)
var matrixEvent = new MatrixEvent(batch[i]);
if (matrixEvent.getId() === earliestEventId) {
foundEventId = true;
debuglog(
"Found timeline[0] event at position %s in batch %s",
i, earliestIndex
);
continue;
}
if (!foundEventId) {
continue;
}
// add entry
debuglog("Add event at position %s in batch %s", i, earliestIndex);
scrollback.push(batch[i]);
if (scrollback.length === limit) {
break;
}
}
if (scrollback.length === limit) {
debuglog("Batch has enough events to satisfy request.");
return scrollback;
}
if (!foundEventId) {
// the earliest index batch didn't contain the event. In other words,
// this timeline is at a state we don't know, so bail.
debuglog(
"Failed to find event ID %s in batch %s", earliestEventId, earliestIndex
);
return [];
}
// get the requested earlier events from earlier batches
while (scrollback.length < limit) {
earliestIndex--;
batch = getItem(
this.store, keyName(room.roomId, "timeline", earliestIndex)
);
if (!batch) {
// no more events
debuglog("No batch found at index %s", earliestIndex);
break;
}
for (i = batch.length - 1; i >= 0; i--) {
debuglog("Add event at position %s in batch %s", i, earliestIndex);
scrollback.push(batch[i]);
if (scrollback.length === limit) {
break;
}
}
}
debuglog(
"Out of %s requested events, returning %s. New index=%s",
limit, scrollback.length, earliestIndex
);
room.addEventsToTimeline(utils.map(scrollback, function(e) {
return new MatrixEvent(e);
}), true, room.getLiveTimeline());
this._tokens[room.storageToken] = {
earliestIndex: earliestIndex
};
return scrollback;
};
/**
* Store events for a room. The events have already been added to the timeline.
* @param {Room} room The room to store events for.
* @param {Array<MatrixEvent>} events The events to store.
* @param {string} token The token associated with these events.
* @param {boolean} toStart True if these are paginated results. The last element
* is the 'oldest' (for parity with homeserver scrollback APIs).
*/
WebStorageStore.prototype.storeEvents = function(room, events, token, toStart) {
if (toStart) {
// add paginated events to lowest batch indexes (can go -ve)
var lowIndex = getIndexExtremity(
getTimelineIndices(this.store, room.roomId), true
);
var i, key, batch;
for (i = 0; i < events.length; i++) { // loop events to be stored
key = keyName(room.roomId, "timeline", lowIndex);
batch = getItem(this.store, key) || [];
while (batch.length < this.batchSize && i < events.length) {
batch.unshift(events[i].event);
i++; // increment to insert next event into this batch
}
i--; // decrement to avoid skipping one (for loop ++s)
setItem(this.store, key, batch);
lowIndex--; // decrement index to get a new batch.
}
}
else {
// dump as live events
var liveEvents = getItem(
this.store, keyName(room.roomId, "timeline", "live")
) || [];
debuglog(
"Adding %s events to %s live list (which has %s already)",
events.length, room.roomId, liveEvents.length
);
var updateState = false;
liveEvents = liveEvents.concat(utils.map(events, function(me) {
// cheeky check to avoid looping twice
if (me.isState()) {
updateState = true;
}
return me.event;
}));
setItem(
this.store, keyName(room.roomId, "timeline", "live"), liveEvents
);
if (updateState) {
debuglog("Storing state for %s as new events updated state", room.roomId);
// use 0 batch size; we don't care about batching right now.
var serRoom = SerialisedRoom.fromRoom(room, 0);
setItem(this.store, keyName(serRoom.roomId, "state"), serRoom.state);
}
}
};
/**
* Sync the 'live' timeline, batching live events according to 'batchSize'.
* @param {string} roomId The room to sync the timeline.
* @param {Array<String>} timelineIndices Optional. The indices in the timeline
* if known already.
*/
WebStorageStore.prototype._syncTimeline = function(roomId, timelineIndices) {
timelineIndices = timelineIndices || getTimelineIndices(this.store, roomId);
var liveEvents = getItem(this.store, keyName(roomId, "timeline", "live")) || [];
// get the highest numbered $INDEX batch
var highestIndex = getIndexExtremity(timelineIndices);
var hiKey = keyName(roomId, "timeline", highestIndex);
var hiBatch = getItem(this.store, hiKey) || [];
// fill up the existing batch first.
while (hiBatch.length < this.batchSize && liveEvents.length > 0) {
hiBatch.push(liveEvents.shift());
}
setItem(this.store, hiKey, hiBatch);
// start adding new batches as required
var batch = [];
while (liveEvents.length > 0) {
batch.push(liveEvents.shift());
if (batch.length === this.batchSize || liveEvents.length === 0) {
// persist the full batch and make another
highestIndex++;
hiKey = keyName(roomId, "timeline", highestIndex);
setItem(this.store, hiKey, batch);
batch = [];
}
}
// reset live array
setItem(this.store, keyName(roomId, "timeline", "live"), []);
};
/**
* Store a filter.
* @param {Filter} filter
*/
WebStorageStore.prototype.storeFilter = function(filter) {
};
/**
* Retrieve a filter.
* @param {string} userId
* @param {string} filterId
* @return {?Filter} A filter or null.
*/
WebStorageStore.prototype.getFilter = function(userId, filterId) {
return null;
};
function SerialisedRoom(roomId) {
this.state = {
events: {}
};
this.timeline = {
// $INDEX: []
};
this.roomId = roomId;
}
/**
* Convert a Room instance into a SerialisedRoom instance which can be stored
* in the key value store.
* @param {Room} room The matrix room to convert
* @param {integer} batchSize The number of events per timeline batch
* @return {SerialisedRoom} A serialised room representation of 'room'.
*/
SerialisedRoom.fromRoom = function(room, batchSize) {
var self = new SerialisedRoom(room.roomId);
var index;
self.state.pagination_token = room.oldState.paginationToken;
// [room_$ROOMID_state] downcast to POJO from MatrixEvent
utils.forEach(utils.keys(room.currentState.events), function(eventType) {
utils.forEach(utils.keys(room.currentState.events[eventType]), function(skey) {
if (!self.state.events[eventType]) {
self.state.events[eventType] = {};
}
self.state.events[eventType][skey] = (
room.currentState.events[eventType][skey].event
);
});
});
// [room_$ROOMID_timeline_$INDEX]
if (batchSize > 0) {
index = 0;
while (index * batchSize < room.timeline.length) {
self.timeline[index] = room.timeline.slice(
index * batchSize, (index + 1) * batchSize
);
self.timeline[index] = utils.map(self.timeline[index], function(me) {
// use POJO not MatrixEvent
return me.event;
});
index++;
}
}
else { // don't batch
self.timeline[0] = utils.map(room.timeline, function(matrixEvent) {
return matrixEvent.event;
});
}
return self;
};
function loadRoom(store, roomId, numEvents, tokenArray) {
var room = new Room(roomId, {
storageToken: tokenArray.length
});
// populate state (flatten nested struct to event array)
var currentStateMap = getItem(store, keyName(roomId, "state"));
var stateEvents = [];
utils.forEach(utils.keys(currentStateMap.events), function(eventType) {
utils.forEach(utils.keys(currentStateMap.events[eventType]), function(skey) {
stateEvents.push(currentStateMap.events[eventType][skey]);
});
});
// TODO: Fix logic dupe with MatrixClient._processRoomEvents
var oldStateEvents = utils.map(
utils.deepCopy(stateEvents), function(e) {
return new MatrixEvent(e);
}
);
var currentStateEvents = utils.map(stateEvents, function(e) {
return new MatrixEvent(e);
}
);
room.oldState.setStateEvents(oldStateEvents);
room.currentState.setStateEvents(currentStateEvents);
// add most recent numEvents
var recentEvents = [];
var index = getIndexExtremity(getTimelineIndices(store, roomId));
var eventIndex = index;
var i, key, batch;
while (recentEvents.length < numEvents) {
key = keyName(roomId, "timeline", index);
batch = getItem(store, key) || [];
if (batch.length === 0) {
// nothing left in the store.
break;
}
for (i = batch.length - 1; i >= 0; i--) {
recentEvents.unshift(new MatrixEvent(batch[i]));
if (recentEvents.length === numEvents) {
eventIndex = index;
break;
}
}
index--;
}
// add events backwards to diverge old state correctly.
room.addEventsToTimeline(recentEvents.reverse(), true, room.getLiveTimeline());
room.oldState.paginationToken = currentStateMap.pagination_token;
// set the token data to let us know which index this room instance is at
// for scrollback.
tokenArray.push({
earliestIndex: eventIndex
});
return room;
}
function persist(store, serRoom) {
setItem(store, keyName(serRoom.roomId, "state"), serRoom.state);
utils.forEach(utils.keys(serRoom.timeline), function(index) {
setItem(store,
keyName(serRoom.roomId, "timeline", index),
serRoom.timeline[index]
);
});
}
function getTimelineIndices(store, roomId) {
var keys = [];
for (var i = 0; i < store.length; i++) {
if (store.key(i).indexOf(keyName(roomId, "timeline_")) !== -1) {
// e.g. room_$ROOMID_timeline_0 => 0
keys.push(
store.key(i).replace(keyName(roomId, "timeline_"), "")
);
}
}
return keys;
}
function getIndexExtremity(timelineIndices, getLowest) {
var extremity, index;
for (var i = 0; i < timelineIndices.length; i++) {
index = parseInt(timelineIndices[i]);
if (!isNaN(index) && (
extremity === undefined ||
!getLowest && index > extremity ||
getLowest && index < extremity)) {
extremity = index;
}
}
return extremity;
}
function keyName(roomId, key, index) {
return "room_" + roomId + "_" + key + (
index === undefined ? "" : ("_" + index)
);
}
function getItem(store, key) {
try {
return JSON.parse(store.getItem(key));
}
catch (e) {
debuglog("Failed to get key %s: %s", key, e);
debuglog(e.stack);
}
return null;
}
function setItem(store, key, val) {
store.setItem(key, JSON.stringify(val));
}
function debuglog() {
if (DEBUG) {
console.log.apply(console, arguments);
}
}
/*
function delRoomStruct(store, roomId) {
var prefix = "room_" + roomId;
var keysToRemove = [];
for (var i = 0; i < store.length; i++) {
if (store.key(i).indexOf(prefix) !== -1) {
keysToRemove.push(store.key(i));
}
}
utils.forEach(keysToRemove, function(key) {
store.removeItem(key);
});
} */
/** Web Storage Store class. */
module.exports = WebStorageStore;
+39 -12
View File
@@ -1,17 +1,20 @@
{
"name": "matrix-js-sdk",
"version": "0.7.1",
"version": "0.7.6-rc.2",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"test": "istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --captureExceptions",
"check": "jasmine-node spec --verbose --junitreport --captureExceptions",
"test:build": "babel -s -d specbuild spec",
"test:run": "istanbul cover --report text --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" _mocha -- --recursive specbuild --colors --reporter mocha-jenkins-reporter --reporter-options junit_report_path=reports/test-results.xml",
"test": "npm run test:build && npm run test:run",
"check": "npm run test:build && _mocha --recursive specbuild --colors",
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
"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",
"start": "babel -s -w -d lib src",
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && uglifyjs -c -m -o dist/browser-matrix.min.js --source-map dist/browser-matrix.min.js.map --in-source-map dist/browser-matrix.js.map dist/browser-matrix.js",
"dist": "npm run build",
"watch": "watchify --exclude olm browser-index.js -o dist/browser-matrix-dev.js -v",
"lint": "jshint -c .jshint lib spec && gjslint --unix_mode --disable 0131,0211,0200,0222,0212 --max_line_length 90 -r spec/ -r lib/",
"prepublish": "git rev-parse HEAD > git-revision.txt"
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
"lint": "eslint --max-warnings 115 src spec",
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt"
},
"repository": {
"url": "https://github.com/matrix-org/matrix-js-sdk"
@@ -23,6 +26,9 @@
"author": "matrix.org",
"license": "Apache-2.0",
"files": [
".babelrc",
".eslintrc.js",
"spec/.eslintrc.js",
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
@@ -37,25 +43,46 @@
"lib",
"package.json",
"release.sh",
"spec"
"spec",
"src"
],
"dependencies": {
"another-json": "^0.2.0",
"browser-request": "^0.3.3",
"browserify": "^10.2.3",
"q": "^1.4.1",
"request": "^2.53.0"
},
"devDependencies": {
"babel-cli": "^6.18.0",
"babel-eslint": "^7.1.1",
"babel-preset-es2015": "^6.18.0",
"browserify": "^14.0.0",
"browserify-shim": "^3.8.13",
"eslint": "^3.13.1",
"eslint-config-google": "^0.7.1",
"exorcist": "^0.4.0",
"expect": "^1.20.2",
"istanbul": "^0.3.13",
"jasmine-node": "^1.14.5",
"jsdoc": "^3.4.0",
"jshint": "^2.8.0",
"lolex": "^1.5.2",
"mocha": "^3.2.0",
"mocha-jenkins-reporter": "^0.3.6",
"rimraf": "^2.5.4",
"source-map-support": "^0.4.11",
"sourceify": "^0.1.0",
"uglifyjs": "^2.4.10",
"watchify": "^3.2.1"
},
"optionalDependencies": {
"olm": "https://matrix.org/packages/npm/olm/olm-1.3.0.tgz"
"olm": "https://matrix.org/packages/npm/olm/olm-2.2.1.tgz"
},
"browserify": {
"transform": [
"sourceify",
"browserify-shim"
]
},
"browserify-shim": {
"olm": "global:Olm"
}
}
+69 -12
View File
@@ -1,12 +1,12 @@
#!/bin/bash
#
# Script to perform a release of matrix-js-sdk. Performs the steps documented
# in RELEASING.md
# Script to perform a release of matrix-js-sdk.
#
# Requires:
# github-changelog-generator; to install, do
# github-changelog-generator; install via:
# 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/)
# jq; install from your distribution's package manager (https://stedolan.github.io/jq/)
# hub; install via brew (OSX) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
set -e
@@ -32,6 +32,16 @@ if [ "$ret" -eq 0 ]; then
exit
fi
if ! git diff-index --quiet --cached HEAD; then
echo "this git checkout has staged (uncommitted) changes. Refusing to release."
exit
fi
if ! git diff-files --quiet; then
echo "this git checkout has uncommitted changes. Refusing to release."
exit
fi
skip_changelog=
skip_jsdoc=
changelog_file="CHANGELOG.md"
@@ -111,19 +121,29 @@ if [ -z "$skip_changelog" ]; then
git commit "$changelog_file" -m "Prepare changelog for $tag"
fi
fi
latest_changes=`mktemp`
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}"
set -x
# Bump package.json, build the dist, and tag
# Bump package.json and build the dist
echo "npm version"
# npm version 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.
# because it can only sign with the default key, but we can
# only turn off both of these behaviours, so we have to
# manually commit the result.
npm version --no-git-tag-version "$release"
git commit package.json -m "$tag"
# figure out if we should be signing this release
signing_id=
if [ -f release_config.yaml ]; then
signing_id=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']"`
fi
# If there is a 'dist' script in the package.json,
# run it in a separate checkout of the project, then
# upload any files in the 'dist' directory as release
@@ -145,9 +165,16 @@ if [ $dodist -eq 0 ]; then
# We haven't tagged yet, so tell the dist script what version
# it's building
DIST_VERSION="$tag" npm run dist
popd
for i in "$builddir"/dist/*; do
assets="$assets -a $i"
if [ -n "$signing_id" ]
then
gpg -u "$signing_id" --armor --output "$i".asc --detach-sig "$i"
assets="$assets -a $i.asc"
fi
done
fi
@@ -155,15 +182,43 @@ fi
# a branch it doesn't have)
git push origin "$rel_branch"
if [ -n "$signing_id" ]; then
# make a signed tag
# gnupg seems to fail to get the right tty device unless we set it here
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
# also make a signature for the source tarball.
project_name=`jq -r '.name' package.json`
source_sigfile="${tag}-src.tar.gz.asc"
git archive --format tgz --prefix="${project_name}-${release}/" "$tag" |
gpg -u "$signing_id" --armor --output "$source_sigfile" --detach-sig -
assets="$assets -a $source_sigfile"
else
git tag -a -F "${latest_changes}" "$tag"
fi
# push the tag
git push origin "$tag"
hubflags=''
if [ $prerelease -eq 1 ]; then
hubflags='-p'
fi
hub release create $hubflags $assets -m "$tag" "$tag"
release_text=`mktemp`
echo "$tag" > "${release_text}"
echo >> "${release_text}"
cat "${latest_changes}" >> "${release_text}"
hub release create $hubflags $assets -f "${release_text}" "$tag"
if [ $dodist -eq 0 ]; then
rm -rf "$builddir"
fi
rm "${release_text}"
rm "${latest_changes}"
# publish to npmjs
npm publish
if [ -z "$skip_jsdoc" ]; then
echo "generating jsdocs"
@@ -180,6 +235,11 @@ if [ -z "$skip_jsdoc" ]; then
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
fi
# if it is a pre-release, leave it on the release branch for now.
if [ $prerelease -eq 1 ]; then
exit 0
fi
# merge release branch to master
echo "updating master branch"
git checkout master
@@ -192,9 +252,6 @@ if [ -z "$skip_jsdoc" ]; then
git push origin gh-pages
fi
# publish to npmjs
npm publish
# finally, merge master back onto develop
git checkout develop
git pull
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env python
"""
Outputs the body of the first entry of changelog file on stdin
"""
import re
import sys
found_first_header = False
for line in sys.stdin:
line = line.strip()
if re.match(r"^Changes in \[.*\]", line):
if found_first_header:
break
found_first_header = True
elif not re.match(r"^=+$", line) and len(line) > 0:
print line
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
env: {
mocha: true,
},
}
+6 -4
View File
@@ -40,14 +40,16 @@ MockStorageApi.prototype = {
return this.keys[index];
},
_recalc: function() {
var keys = [];
for (var k in this.data) {
if (!this.data.hasOwnProperty(k)) { continue; }
const keys = [];
for (const k in this.data) {
if (!this.data.hasOwnProperty(k)) {
continue;
}
keys.push(k);
}
this.keys = keys;
this.length = keys.length;
}
},
};
/** */
+185
View File
@@ -0,0 +1,185 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import sdk from '..';
import testUtils from './test-utils';
import MockHttpBackend from './mock-request';
import expect from 'expect';
import q from 'q';
/**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
*
* @constructor
* @param {string} userId
* @param {string} deviceId
* @param {string} accessToken
*/
export default function TestClient(userId, deviceId, accessToken) {
this.userId = userId;
this.deviceId = deviceId;
this.storage = new sdk.WebStorageSessionStore(new testUtils.MockStorageApi());
this.httpBackend = new MockHttpBackend();
this.client = sdk.createClient({
baseUrl: "http://" + userId + ".test.server",
userId: userId,
accessToken: accessToken,
deviceId: deviceId,
sessionStore: this.storage,
request: this.httpBackend.requestFn,
});
this.deviceKeys = null;
this.oneTimeKeys = {};
}
TestClient.prototype.toString = function() {
return 'TestClient[' + this.userId + ']';
};
/**
* start the client, and wait for it to initialise.
*
* @return {Promise}
*/
TestClient.prototype.start = function() {
this.httpBackend.when("GET", "/pushrules").respond(200, {});
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
this.expectDeviceKeyUpload();
// we let the client do a very basic initial sync, which it needs before
// it will upload one-time keys.
this.httpBackend.when("GET", "/sync").respond(200, { next_batch: 1 });
this.client.startClient({
// set this so that we can get hold of failed events
pendingEventOrdering: 'detached',
});
return this.httpBackend.flush().then(() => {
console.log(this + ': started');
});
};
/**
* stop the client
*/
TestClient.prototype.stop = function() {
this.client.stopClient();
};
/**
* Set up expectations that the client will upload device keys.
*/
TestClient.prototype.expectDeviceKeyUpload = function() {
const self = this;
this.httpBackend.when("POST", "/keys/upload").respond(200, function(path, content) {
expect(content.one_time_keys).toBe(undefined);
expect(content.device_keys).toBeTruthy();
console.log(self + ': received device keys');
// we expect this to happen before any one-time keys are uploaded.
expect(Object.keys(self.oneTimeKeys).length).toEqual(0);
self.deviceKeys = content.device_keys;
return {one_time_key_counts: {signed_curve25519: 0}};
});
};
/**
* If one-time keys have already been uploaded, return them. Otherwise,
* set up an expectation that the keys will be uploaded, and wait for
* that to happen.
*
* @returns {Promise} for the one-time keys
*/
TestClient.prototype.awaitOneTimeKeyUpload = function() {
if (Object.keys(this.oneTimeKeys).length != 0) {
// already got one-time keys
return q(this.oneTimeKeys);
}
this.httpBackend.when("POST", "/keys/upload")
.respond(200, (path, content) => {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBe(undefined);
return {one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
}};
});
this.httpBackend.when("POST", "/keys/upload")
.respond(200, (path, content) => {
expect(content.device_keys).toBe(undefined);
expect(content.one_time_keys).toBeTruthy();
expect(content.one_time_keys).toNotEqual({});
console.log('%s: received %i one-time keys', this,
Object.keys(content.one_time_keys).length);
this.oneTimeKeys = content.one_time_keys;
return {one_time_key_counts: {
signed_curve25519: Object.keys(this.oneTimeKeys).length,
}};
});
return this.httpBackend.flush('/keys/upload', 2).then((flushed) => {
expect(flushed).toEqual(2);
return this.oneTimeKeys;
});
};
/**
* Set up expectations that the client will query device keys.
*
* We check that the query contains each of the users in `response`.
*
* @param {Object} response response to the query.
*/
TestClient.prototype.expectKeyQuery = function(response) {
this.httpBackend.when('POST', '/keys/query').respond(
200, (path, content) => {
Object.keys(response.device_keys).forEach((userId) => {
expect(content.device_keys[userId]).toEqual({});
});
return response;
});
};
/**
* get the uploaded curve25519 device key
*
* @return {string} base64 device key
*/
TestClient.prototype.getDeviceKey = function() {
const keyId = 'curve25519:' + this.deviceId;
return this.deviceKeys.keys[keyId];
};
/**
* get the uploaded ed25519 device key
*
* @return {string} base64 device key
*/
TestClient.prototype.getSigningKey = function() {
const keyId = 'ed25519:' + this.deviceId;
return this.deviceKeys.keys[keyId];
};
File diff suppressed because it is too large Load Diff
+82 -76
View File
@@ -1,22 +1,26 @@
"use strict";
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const utils = require("../test-utils");
import expect from 'expect';
describe("MatrixClient events", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var selfUserId = "@alice:localhost";
var selfAccessToken = "aseukfgwef";
const baseUrl = "http://localhost.or.something";
let client;
let httpBackend;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: selfUserId,
accessToken: selfAccessToken
accessToken: selfAccessToken,
});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
@@ -28,14 +32,14 @@ describe("MatrixClient events", function() {
});
describe("emissions", function() {
var SYNC_DATA = {
const SYNC_DATA = {
next_batch: "s_5_3",
presence: {
events: [
utils.mkPresence({
user: "@foo:bar", name: "Foo Bar", presence: "online"
})
]
user: "@foo:bar", name: "Foo Bar", presence: "online",
}),
],
},
rooms: {
join: {
@@ -43,30 +47,30 @@ describe("MatrixClient events", function() {
timeline: {
events: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm"
})
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm",
}),
],
prev_batch: "s"
prev_batch: "s",
},
state: {
events: [
utils.mkMembership({
room: "!erufh:bar", mship: "join", user: "@foo:bar"
room: "!erufh:bar", mship: "join", user: "@foo:bar",
}),
utils.mkEvent({
type: "m.room.create", room: "!erufh:bar",
user: "@foo:bar",
content: {
creator: "@foo:bar"
}
})
]
}
}
}
}
creator: "@foo:bar",
},
}),
],
},
},
},
},
};
var NEXT_SYNC_DATA = {
const NEXT_SYNC_DATA = {
next_batch: "e_6_7",
rooms: {
join: {
@@ -74,25 +78,26 @@ describe("MatrixClient events", function() {
timeline: {
events: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "ello ello"
room: "!erufh:bar", user: "@foo:bar",
msg: "ello ello",
}),
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: ":D"
room: "!erufh:bar", user: "@foo:bar", msg: ":D",
}),
]
],
},
ephemeral: {
events: [
utils.mkEvent({
type: "m.typing", room: "!erufh:bar", content: {
user_ids: ["@foo:bar"]
}
})
]
}
}
}
}
user_ids: ["@foo:bar"],
},
}),
],
},
},
},
},
};
it("should emit events from both the first and subsequent /sync calls",
@@ -100,18 +105,18 @@ describe("MatrixClient events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var expectedEvents = [];
let expectedEvents = [];
expectedEvents = expectedEvents.concat(
SYNC_DATA.presence.events,
SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
SYNC_DATA.rooms.join["!erufh:bar"].state.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].timeline.events,
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events
NEXT_SYNC_DATA.rooms.join["!erufh:bar"].ephemeral.events,
);
client.on("event", function(event) {
var found = false;
for (var i = 0; i < expectedEvents.length; i++) {
let found = false;
for (let i = 0; i < expectedEvents.length; i++) {
if (expectedEvents[i].event_id === event.getId()) {
expectedEvents.splice(i, 1);
found = true;
@@ -119,7 +124,7 @@ describe("MatrixClient events", function() {
}
}
expect(found).toBe(
true, "Unexpected 'event' emitted: " + event.getType()
true, "Unexpected 'event' emitted: " + event.getType(),
);
});
@@ -127,7 +132,7 @@ describe("MatrixClient events", function() {
httpBackend.flush().done(function() {
expect(expectedEvents.length).toEqual(
0, "Failed to see all events from /sync calls"
0, "Failed to see all events from /sync calls",
);
done();
});
@@ -136,16 +141,18 @@ describe("MatrixClient events", function() {
it("should emit User events", function(done) {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var fired = false;
let fired = false;
client.on("User.presence", function(event, user) {
fired = true;
expect(user).toBeDefined();
expect(event).toBeDefined();
if (!user || !event) { return; }
expect(user).toBeTruthy();
expect(event).toBeTruthy();
if (!user || !event) {
return;
}
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
expect(user.presence).toEqual(
SYNC_DATA.presence.events[0].content.presence
SYNC_DATA.presence.events[0].content.presence,
);
});
client.startClient();
@@ -159,9 +166,9 @@ describe("MatrixClient events", function() {
it("should emit Room events", function(done) {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var roomInvokeCount = 0;
var roomNameInvokeCount = 0;
var timelineFireCount = 0;
let roomInvokeCount = 0;
let roomNameInvokeCount = 0;
let timelineFireCount = 0;
client.on("Room", function(room) {
roomInvokeCount++;
expect(room.roomId).toEqual("!erufh:bar");
@@ -178,13 +185,13 @@ describe("MatrixClient events", function() {
httpBackend.flush().done(function() {
expect(roomInvokeCount).toEqual(
1, "Room fired wrong number of times."
1, "Room fired wrong number of times.",
);
expect(roomNameInvokeCount).toEqual(
1, "Room.name fired wrong number of times."
1, "Room.name fired wrong number of times.",
);
expect(timelineFireCount).toEqual(
3, "Room.timeline fired the wrong number of times"
3, "Room.timeline fired the wrong number of times",
);
done();
});
@@ -194,17 +201,17 @@ describe("MatrixClient events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var roomStateEventTypes = [
"m.room.member", "m.room.create"
const roomStateEventTypes = [
"m.room.member", "m.room.create",
];
var eventsInvokeCount = 0;
var membersInvokeCount = 0;
var newMemberInvokeCount = 0;
let eventsInvokeCount = 0;
let membersInvokeCount = 0;
let newMemberInvokeCount = 0;
client.on("RoomState.events", function(event, state) {
eventsInvokeCount++;
var index = roomStateEventTypes.indexOf(event.getType());
expect(index).not.toEqual(
-1, "Unexpected room state event type: " + event.getType()
const index = roomStateEventTypes.indexOf(event.getType());
expect(index).toNotEqual(
-1, "Unexpected room state event type: " + event.getType(),
);
if (index >= 0) {
roomStateEventTypes.splice(index, 1);
@@ -227,13 +234,13 @@ describe("MatrixClient events", function() {
httpBackend.flush().done(function() {
expect(membersInvokeCount).toEqual(
1, "RoomState.members fired wrong number of times"
1, "RoomState.members fired wrong number of times",
);
expect(newMemberInvokeCount).toEqual(
1, "RoomState.newMember fired wrong number of times"
1, "RoomState.newMember fired wrong number of times",
);
expect(eventsInvokeCount).toEqual(
2, "RoomState.events fired wrong number of times"
2, "RoomState.events fired wrong number of times",
);
done();
});
@@ -243,10 +250,10 @@ describe("MatrixClient events", function() {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var typingInvokeCount = 0;
var powerLevelInvokeCount = 0;
var nameInvokeCount = 0;
var membershipInvokeCount = 0;
let typingInvokeCount = 0;
let powerLevelInvokeCount = 0;
let nameInvokeCount = 0;
let membershipInvokeCount = 0;
client.on("RoomMember.name", function(event, member) {
nameInvokeCount++;
});
@@ -266,16 +273,16 @@ describe("MatrixClient events", function() {
httpBackend.flush().done(function() {
expect(typingInvokeCount).toEqual(
1, "RoomMember.typing fired wrong number of times"
1, "RoomMember.typing fired wrong number of times",
);
expect(powerLevelInvokeCount).toEqual(
0, "RoomMember.powerLevel fired wrong number of times"
0, "RoomMember.powerLevel fired wrong number of times",
);
expect(nameInvokeCount).toEqual(
0, "RoomMember.name fired wrong number of times"
0, "RoomMember.name fired wrong number of times",
);
expect(membershipInvokeCount).toEqual(
1, "RoomMember.membership fired wrong number of times"
1, "RoomMember.membership fired wrong number of times",
);
done();
});
@@ -284,7 +291,7 @@ describe("MatrixClient events", function() {
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function(done) {
httpBackend.when("GET", "/sync").respond(401, { errcode: 'M_UNKNOWN_TOKEN' });
var sessionLoggedOutCount = 0;
let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(event, member) {
sessionLoggedOutCount++;
});
@@ -293,11 +300,10 @@ describe("MatrixClient events", function() {
httpBackend.flush().done(function() {
expect(sessionLoggedOutCount).toEqual(
1, "Session.logged_out fired wrong number of times"
1, "Session.logged_out fired wrong number of times",
);
done();
});
});
});
});
+110 -96
View File
@@ -1,29 +1,30 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
var EventTimeline = sdk.EventTimeline;
import 'source-map-support/register';
const q = require("q");
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const utils = require("../test-utils");
const EventTimeline = sdk.EventTimeline;
var baseUrl = "http://localhost.or.something";
var userId = "@alice:localhost";
var userName = "Alice";
var accessToken = "aseukfgwef";
var roomId = "!foo:bar";
var otherUserId = "@bob:localhost";
const baseUrl = "http://localhost.or.something";
const userId = "@alice:localhost";
const userName = "Alice";
const accessToken = "aseukfgwef";
const roomId = "!foo:bar";
const otherUserId = "@bob:localhost";
var USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName,
});
var ROOM_NAME_EVENT = utils.mkEvent({
const ROOM_NAME_EVENT = utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId,
content: {
name: "Old room name"
}
name: "Old room name",
},
});
var INITIAL_SYNC_DATA = {
const INITIAL_SYNC_DATA = {
next_batch: "s_5_3",
rooms: {
join: {
@@ -31,33 +32,33 @@ var INITIAL_SYNC_DATA = {
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello"
})
room: roomId, user: otherUserId, msg: "hello",
}),
],
prev_batch: "f_1_1"
prev_batch: "f_1_1",
},
state: {
events: [
ROOM_NAME_EVENT,
utils.mkMembership({
room: roomId, mship: "join",
user: otherUserId, name: "Bob"
user: otherUserId, name: "Bob",
}),
USER_MEMBERSHIP_EVENT,
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}
}
}
}
creator: userId,
},
}),
],
},
},
},
},
};
var EVENTS = [
const EVENTS = [
utils.mkMessage({
room: roomId, user: userId, msg: "we",
}),
@@ -81,7 +82,7 @@ function startClient(httpBackend, client) {
client.startClient();
// set up a promise which will resolve once the client is initialised
var deferred = q.defer();
const deferred = q.defer();
client.on("sync", function(state) {
console.log("sync", state);
if (state != "SYNCING") {
@@ -94,13 +95,12 @@ function startClient(httpBackend, client) {
return deferred.promise;
}
describe("getEventTimeline support", function() {
var httpBackend;
var client;
let httpBackend;
let client;
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
});
@@ -118,12 +118,13 @@ describe("getEventTimeline support", function() {
accessToken: accessToken,
});
startClient(httpBackend, client
startClient(httpBackend, client,
).then(function() {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
expect(function() { client.getEventTimeline(timelineSet, "event"); })
.toThrow();
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
expect(function() {
client.getEventTimeline(timelineSet, "event");
}).toThrow();
}).catch(utils.failTest).done(done);
});
@@ -135,12 +136,13 @@ describe("getEventTimeline support", function() {
timelineSupport: true,
});
startClient(httpBackend, client
startClient(httpBackend, client,
).then(function() {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
expect(function() { client.getEventTimeline(timelineSet, "event"); })
.not.toThrow();
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
expect(function() {
client.getEventTimeline(timelineSet, "event");
}).toNotThrow();
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
@@ -155,9 +157,9 @@ describe("getEventTimeline support", function() {
userId: userId,
accessToken: accessToken,
});
var room;
let room;
startClient(httpBackend, client
startClient(httpBackend, client,
).then(function() {
room = client.getRoom(roomId);
@@ -217,11 +219,14 @@ describe("getEventTimeline support", function() {
});
});
import expect from 'expect';
describe("MatrixClient event timelines", function() {
var client, httpBackend;
let client = null;
let httpBackend = null;
beforeEach(function(done) {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
@@ -243,8 +248,8 @@ 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];
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
.respond(200, function() {
return {
@@ -262,7 +267,7 @@ describe("MatrixClient event timelines", function() {
client.getEventTimeline(timelineSet, "event1:bar").then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
for (var i = 0; i < 4; i++) {
for (let i = 0; i < 4; i++) {
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
expect(tl.getEvents()[i].sender.name).toEqual(userName);
}
@@ -276,8 +281,8 @@ 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];
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
@@ -307,9 +312,9 @@ describe("MatrixClient event timelines", function() {
httpBackend.flush().catch(utils.failTest);
});
it("should update timelines where they overlap a previous /sync", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
it("should update timelines where they overlap a previous /sync", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
@@ -339,8 +344,9 @@ describe("MatrixClient event timelines", function() {
};
});
const deferred = q.defer();
client.on("sync", function() {
client.getEventTimeline(timelineSet, 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]);
@@ -350,16 +356,20 @@ describe("MatrixClient event timelines", function() {
.toEqual("start_token");
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
// .toEqual("s_5_4");
}).catch(utils.failTest).done(done);
}).done(() => deferred.resolve(),
(e) => deferred.reject(e));
});
httpBackend.flush().catch(utils.failTest);
return q.all([
httpBackend.flush(),
deferred.promise,
]);
});
it("should join timelines where they overlap a previous /context",
function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
// with context which joins them all up.
@@ -415,15 +425,15 @@ describe("MatrixClient event timelines", function() {
};
});
var tl0, tl2, tl3;
client.getEventTimeline(timelineSet, EVENTS[0].event_id
let tl0;
let tl3;
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl0 = tl;
return client.getEventTimeline(timelineSet, EVENTS[2].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl2 = tl;
return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
@@ -452,8 +462,8 @@ 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];
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
// we fetch event 0, then 2, then 3, and finally 1. 1 is returned
// with context which joins them all up.
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1")
@@ -467,7 +477,7 @@ describe("MatrixClient event timelines", function() {
};
});
client.getEventTimeline(timelineSet, "event1"
client.getEventTimeline(timelineSet, "event1",
).then(function(tl) {
// could do with a fail()
expect(true).toBeFalsy();
@@ -481,8 +491,8 @@ 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];
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
@@ -499,7 +509,7 @@ describe("MatrixClient event timelines", function() {
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
.check(function(req) {
var params = req.queryParams;
const params = req.queryParams;
expect(params.dir).toEqual("b");
expect(params.from).toEqual("start_token0");
expect(params.limit).toEqual(30);
@@ -510,8 +520,8 @@ describe("MatrixClient event timelines", function() {
};
});
var tl;
client.getEventTimeline(timelineSet, EVENTS[0].event_id
let tl;
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(tl, {backwards: true});
@@ -532,8 +542,8 @@ describe("MatrixClient event timelines", function() {
it("should allow you to paginate forwards", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
@@ -550,7 +560,7 @@ describe("MatrixClient event timelines", function() {
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
.check(function(req) {
var params = req.queryParams;
const params = req.queryParams;
expect(params.dir).toEqual("f");
expect(params.from).toEqual("end_token0");
expect(params.limit).toEqual(20);
@@ -561,8 +571,8 @@ describe("MatrixClient event timelines", function() {
};
});
var tl;
client.getEventTimeline(timelineSet, EVENTS[0].event_id
let tl;
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(
@@ -584,8 +594,8 @@ describe("MatrixClient event timelines", function() {
});
describe("event timeline for sent events", function() {
var TXN_ID = "txn1";
var event = utils.mkMessage({
const TXN_ID = "txn1";
const event = utils.mkMessage({
room: roomId, user: userId, msg: "a body",
});
event.unsigned = {transaction_id: TXN_ID};
@@ -604,7 +614,7 @@ describe("MatrixClient event timelines", function() {
"!foo:bar": {
timeline: {
events: [
event
event,
],
prev_batch: "f_1_1",
},
@@ -615,8 +625,8 @@ 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];
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
expect(res.event_id).toEqual(event.event_id);
@@ -628,6 +638,8 @@ describe("MatrixClient event timelines", function() {
// now let the sync complete, and check it again
return httpBackend.flush("/sync", 1);
}).then(function() {
return utils.syncPromise(client);
}).then(function() {
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
@@ -639,8 +651,8 @@ 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];
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
// initiate the send, and set up checks to be done when it completes
// - but note that it won't complete until after the /sync does, below.
@@ -655,6 +667,8 @@ describe("MatrixClient event timelines", function() {
}).catch(utils.failTest).done(done);
httpBackend.flush("/sync", 1).then(function() {
return utils.syncPromise(client);
}).then(function() {
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (1)");
@@ -672,18 +686,18 @@ describe("MatrixClient event timelines", function() {
// 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
const event = utils.mkMembership({
room: roomId, mship: "join", user: otherUserId,
});
var redaction = utils.mkEvent({
const redaction = utils.mkEvent({
type: "m.room.redaction",
room_id: roomId,
sender: otherUserId,
content: {}
content: {},
});
redaction.redacts = event.event_id;
var syncData = {
const syncData = {
next_batch: "batch1",
rooms: {
join: {},
@@ -701,12 +715,12 @@ describe("MatrixClient event timelines", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.flush().then(function() {
var room = client.getRoom(roomId);
var tl = room.getLiveTimeline();
const room = client.getRoom(roomId);
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[1].isRedacted()).toBe(true);
var sync2 = {
const sync2 = {
next_batch: "batch2",
rooms: {
join: {},
@@ -716,7 +730,7 @@ describe("MatrixClient event timelines", function() {
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "world"
room: roomId, user: otherUserId, msg: "world",
}),
],
limited: true,
@@ -727,8 +741,8 @@ describe("MatrixClient event timelines", function() {
return httpBackend.flush();
}).then(function() {
var room = client.getRoom(roomId);
var tl = room.getLiveTimeline();
const room = client.getRoom(roomId);
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(1);
}).catch(utils.failTest).done(done);
});
+85 -77
View File
@@ -1,25 +1,31 @@
"use strict";
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var publicGlobals = require("../../lib/matrix");
var Room = publicGlobals.Room;
var MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
var Filter = publicGlobals.Filter;
var utils = require("../test-utils");
var MockStorageApi = require("../MockStorageApi");
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const publicGlobals = require("../../lib/matrix");
const Room = publicGlobals.Room;
const MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
const Filter = publicGlobals.Filter;
const utils = require("../test-utils");
const MockStorageApi = require("../MockStorageApi");
import expect from 'expect';
describe("MatrixClient", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend, store, sessionStore;
var userId = "@alice:localhost";
var accessToken = "aseukfgwef";
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
let store = null;
let sessionStore = null;
const userId = "@alice:localhost";
const accessToken = "aseukfgwef";
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
store = new MatrixInMemoryStore();
var mockStorage = new MockStorageApi();
const mockStorage = new MockStorageApi();
sessionStore = new sdk.WebStorageSessionStore(mockStorage);
sdk.request(httpBackend.requestFn);
@@ -38,10 +44,10 @@ describe("MatrixClient", function() {
});
describe("uploadContent", function() {
var buf = new Buffer('hello world');
const buf = new Buffer('hello world');
it("should upload the file", function(done) {
httpBackend.when(
"POST", "/_matrix/media/v1/upload"
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.data).toEqual(buf);
expect(req.queryParams.filename).toEqual("hi.txt");
@@ -51,15 +57,15 @@ describe("MatrixClient", function() {
expect(req.opts.timeout).toBe(undefined);
}).respond(200, "content");
var prom = client.uploadContent({
const prom = client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
});
expect(prom).toBeDefined();
expect(prom).toBeTruthy();
var uploads = client.getCurrentUploads();
const uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(1);
expect(uploads[0].promise).toBe(prom);
expect(uploads[0].loaded).toEqual(0);
@@ -68,7 +74,7 @@ describe("MatrixClient", function() {
// for backwards compatibility, we return the raw JSON
expect(response).toEqual("content");
var uploads = client.getCurrentUploads();
const uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(0);
}).catch(utils.failTest).done(done);
@@ -77,7 +83,7 @@ describe("MatrixClient", function() {
it("should parse the response if rawResponse=false", function(done) {
httpBackend.when(
"POST", "/_matrix/media/v1/upload"
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.opts.json).toBeFalsy();
}).respond(200, JSON.stringify({ "content_uri": "uri" }));
@@ -98,7 +104,7 @@ describe("MatrixClient", function() {
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"
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.data).toEqual(buf);
expect(req.opts.json).toBeFalsy();
@@ -123,13 +129,13 @@ describe("MatrixClient", function() {
});
it("should return a promise which can be cancelled", function(done) {
var prom = client.uploadContent({
const prom = client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
});
var uploads = client.getCurrentUploads();
const uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(1);
expect(uploads[0].promise).toBe(prom);
expect(uploads[0].loaded).toEqual(0);
@@ -139,23 +145,23 @@ describe("MatrixClient", function() {
}, function(error) {
expect(error).toEqual("aborted");
var uploads = client.getCurrentUploads();
const uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(0);
}).catch(utils.failTest).done(done);
var r = client.cancelUpload(prom);
const r = client.cancelUpload(prom);
expect(r).toBe(true);
});
});
describe("joinRoom", function() {
it("should no-op if you've already joined a room", function() {
var roomId = "!foo:bar";
var room = new Room(roomId);
const roomId = "!foo:bar";
const room = new Room(roomId);
room.addLiveEvents([
utils.mkMembership({
user: userId, room: roomId, mship: "join", event: true
})
user: userId, room: roomId, mship: "join", event: true,
}),
]);
store.storeRoom(room);
client.joinRoom(roomId);
@@ -164,11 +170,11 @@ describe("MatrixClient", function() {
});
describe("getFilter", function() {
var filterId = "f1lt3r1d";
const filterId = "f1lt3r1d";
it("should return a filter from the store if allowCached", function(done) {
var filter = Filter.fromJson(userId, filterId, {
event_format: "client"
const filter = Filter.fromJson(userId, filterId, {
event_format: "client",
});
store.storeFilter(filter);
client.getFilter(userId, filterId, true).done(function(gotFilter) {
@@ -180,16 +186,16 @@ describe("MatrixClient", function() {
it("should do an HTTP request if !allowCached even if one exists",
function(done) {
var httpFilterDefinition = {
event_format: "federation"
const httpFilterDefinition = {
event_format: "federation",
};
httpBackend.when(
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
).respond(200, httpFilterDefinition);
var storeFilter = Filter.fromJson(userId, filterId, {
event_format: "client"
const storeFilter = Filter.fromJson(userId, filterId, {
event_format: "client",
});
store.storeFilter(storeFilter);
client.getFilter(userId, filterId, false).done(function(gotFilter) {
@@ -202,17 +208,17 @@ describe("MatrixClient", function() {
it("should do an HTTP request if nothing is in the cache and then store it",
function(done) {
var httpFilterDefinition = {
event_format: "federation"
const httpFilterDefinition = {
event_format: "federation",
};
expect(store.getFilter(userId, filterId)).toBeNull();
expect(store.getFilter(userId, filterId)).toBe(null);
httpBackend.when(
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
).respond(200, httpFilterDefinition);
client.getFilter(userId, filterId, true).done(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
expect(store.getFilter(userId, filterId)).toBeDefined();
expect(store.getFilter(userId, filterId)).toBeTruthy();
done();
});
@@ -221,21 +227,21 @@ describe("MatrixClient", function() {
});
describe("createFilter", function() {
var filterId = "f1llllllerid";
const filterId = "f1llllllerid";
it("should do an HTTP request and then store the filter", function(done) {
expect(store.getFilter(userId, filterId)).toBeNull();
expect(store.getFilter(userId, filterId)).toBe(null);
var filterDefinition = {
event_format: "client"
const filterDefinition = {
event_format: "client",
};
httpBackend.when(
"POST", "/user/" + encodeURIComponent(userId) + "/filter"
"POST", "/user/" + encodeURIComponent(userId) + "/filter",
).check(function(req) {
expect(req.data).toEqual(filterDefinition);
}).respond(200, {
filter_id: filterId
filter_id: filterId,
});
client.createFilter(filterDefinition).done(function(gotFilter) {
@@ -249,8 +255,7 @@ describe("MatrixClient", function() {
});
describe("searching", function() {
var response = {
const response = {
search_categories: {
room_events: {
count: 24,
@@ -263,26 +268,26 @@ describe("MatrixClient", function() {
room_id: "!feuiwhf:localhost",
content: {
body: "a result",
msgtype: "m.text"
}
}
}
}
}
}
msgtype: "m.text",
},
},
},
},
},
},
};
it("searchMessageText should perform a /search for room_events", function(done) {
client.searchMessageText({
query: "monkeys"
query: "monkeys",
});
httpBackend.when("POST", "/search").check(function(req) {
expect(req.data).toEqual({
search_categories: {
room_events: {
search_term: "monkeys"
}
}
search_term: "monkeys",
},
},
});
}).respond(200, response);
@@ -295,9 +300,9 @@ describe("MatrixClient", function() {
describe("downloadKeys", function() {
it("should do an HTTP request and then store the keys", function(done) {
var ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
// ed25519key = client.getDeviceEd25519Key();
var borisKeys = {
const borisKeys = {
dev1: {
algorithms: ["1"],
device_id: "dev1",
@@ -306,14 +311,14 @@ describe("MatrixClient", function() {
boris: {
"ed25519:dev1":
"RAhmbNDq1efK3hCpBzZDsKoGSsrHUxb25NW5/WbEV9R" +
"JVwLdP032mg5QsKt/pBDUGtggBcnk43n3nBWlA88WAw"
}
"JVwLdP032mg5QsKt/pBDUGtggBcnk43n3nBWlA88WAw",
},
},
unsigned: { "abc": "def" },
user_id: "boris",
}
},
};
var chazKeys = {
const chazKeys = {
dev2: {
algorithms: ["2"],
device_id: "dev2",
@@ -322,12 +327,12 @@ describe("MatrixClient", function() {
chaz: {
"ed25519:dev2":
"FwslH/Q7EYSb7swDJbNB5PSzcbEO1xRRBF1riuijqvL" +
"EkrK9/XVN8jl4h7thGuRITQ01siBQnNmMK9t45QfcCQ"
}
"EkrK9/XVN8jl4h7thGuRITQ01siBQnNmMK9t45QfcCQ",
},
},
unsigned: { "ghi": "def" },
user_id: "chaz",
}
},
};
/*
@@ -345,7 +350,10 @@ describe("MatrixClient", function() {
*/
httpBackend.when("POST", "/keys/query").check(function(req) {
expect(req.data).toEqual({device_keys: {boris: {}, chaz: {}}});
expect(req.data).toEqual({device_keys: {
'boris': {},
'chaz': {},
}});
}).respond(200, {
device_keys: {
boris: borisKeys,
@@ -363,7 +371,7 @@ describe("MatrixClient", function() {
assertObjectContains(res.chaz.dev2, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev2" : ed25519key },
keys: { "ed25519:dev2": ed25519key },
algorithms: ["2"],
unsigned: { "ghi": "def" },
});
@@ -374,16 +382,16 @@ describe("MatrixClient", function() {
});
describe("deleteDevice", function() {
var auth = {a: 1};
const auth = {a: 1};
it("should pass through an auth dict", function(done) {
httpBackend.when(
"DELETE", "/_matrix/client/unstable/devices/my_device"
"DELETE", "/_matrix/client/unstable/devices/my_device",
).check(function(req) {
expect(req.data).toEqual({auth: auth});
}).respond(200);
client.deleteDevice(
"my_device", auth
"my_device", auth,
).catch(utils.failTest).done(done);
httpBackend.flush();
@@ -392,7 +400,7 @@ describe("MatrixClient", function() {
});
function assertObjectContains(obj, expected) {
for (var k in expected) {
for (const k in expected) {
if (expected.hasOwnProperty(k)) {
expect(obj[k]).toEqual(expected[k]);
}
+49 -43
View File
@@ -1,17 +1,21 @@
"use strict";
var sdk = require("../..");
var MatrixClient = sdk.MatrixClient;
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
import 'source-map-support/register';
const sdk = require("../..");
const MatrixClient = sdk.MatrixClient;
const HttpBackend = require("../mock-request");
const utils = require("../test-utils");
import expect from 'expect';
describe("MatrixClient opts", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var userId = "@alice:localhost";
var userB = "@bob:localhost";
var accessToken = "aseukfgwef";
var roomId = "!foo:bar";
var syncData = {
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
const userId = "@alice:localhost";
const userB = "@bob:localhost";
const accessToken = "aseukfgwef";
const roomId = "!foo:bar";
const syncData = {
next_batch: "s_5_3",
presence: {},
rooms: {
@@ -20,40 +24,40 @@ describe("MatrixClient opts", function() {
timeline: {
events: [
utils.mkMessage({
room: roomId, user: userB, msg: "hello"
})
room: roomId, user: userB, msg: "hello",
}),
],
prev_batch: "f_1_1"
prev_batch: "f_1_1",
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB,
content: {
name: "Old room name"
}
name: "Old room name",
},
}),
utils.mkMembership({
room: roomId, mship: "join", user: userB, name: "Bob"
room: roomId, mship: "join", user: userB, name: "Bob",
}),
utils.mkMembership({
room: roomId, mship: "join", user: userId, name: "Alice"
room: roomId, mship: "join", user: userId, name: "Alice",
}),
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}
}
}
}
creator: userId,
},
}),
],
},
},
},
},
};
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
});
@@ -69,7 +73,7 @@ describe("MatrixClient opts", function() {
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
scheduler: new sdk.MatrixScheduler()
scheduler: new sdk.MatrixScheduler(),
});
});
@@ -78,9 +82,9 @@ describe("MatrixClient opts", function() {
});
it("should be able to send messages", function(done) {
var eventId = "$flibble:wibble";
const eventId = "$flibble:wibble";
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId
event_id: eventId,
});
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
expect(res.event_id).toEqual(eventId);
@@ -90,16 +94,16 @@ describe("MatrixClient opts", function() {
});
it("should be able to sync / get new events", function(done) {
var expectedEventTypes = [ // from /initialSync
const expectedEventTypes = [ // from /initialSync
"m.room.message", "m.room.name", "m.room.member", "m.room.member",
"m.room.create"
"m.room.create",
];
client.on("event", function(event) {
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
-1, "Recv unexpected event type: " + event.getType()
expect(expectedEventTypes.indexOf(event.getType())).toNotEqual(
-1, "Recv unexpected event type: " + event.getType(),
);
expectedEventTypes.splice(
expectedEventTypes.indexOf(event.getType()), 1
expectedEventTypes.indexOf(event.getType()), 1,
);
});
httpBackend.when("GET", "/pushrules").respond(200, {});
@@ -110,9 +114,11 @@ describe("MatrixClient opts", function() {
return httpBackend.flush("/filter", 1);
}).then(function() {
return httpBackend.flush("/sync", 1);
}).then(function() {
return utils.syncPromise(client);
}).done(function() {
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes
0, "Expected to see event types: " + expectedEventTypes,
);
done();
});
@@ -127,14 +133,14 @@ describe("MatrixClient opts", function() {
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
scheduler: undefined
scheduler: undefined,
});
});
it("shouldn't retry sending events", function(done) {
httpBackend.when("PUT", "/txn1").fail(500, {
errcode: "M_SOMETHING",
error: "Ruh roh"
error: "Ruh roh",
});
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
expect(false).toBe(true, "sendTextMessage resolved but shouldn't");
@@ -147,13 +153,13 @@ describe("MatrixClient opts", function() {
it("shouldn't queue events", function(done) {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: "AAA"
event_id: "AAA",
});
httpBackend.when("PUT", "/txn2").respond(200, {
event_id: "BBB"
event_id: "BBB",
});
var sentA = false;
var sentB = false;
let sentA = false;
let sentB = false;
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
sentA = true;
expect(sentB).toBe(true);
@@ -171,7 +177,7 @@ describe("MatrixClient opts", function() {
it("should be able to send messages", function(done) {
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: "foo"
event_id: "foo",
});
client.sendTextMessage("!foo:bar", "a body", "txn1").done(function(res) {
expect(res.event_id).toEqual("foo");
+22 -19
View File
@@ -1,20 +1,24 @@
"use strict";
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
var EventStatus = sdk.EventStatus;
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const utils = require("../test-utils");
const EventStatus = sdk.EventStatus;
import expect from 'expect';
describe("MatrixClient retrying", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var scheduler;
var userId = "@alice:localhost";
var accessToken = "aseukfgwef";
var roomId = "!room:here";
var room;
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
let scheduler;
const userId = "@alice:localhost";
const accessToken = "aseukfgwef";
const roomId = "!room:here";
let room;
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
scheduler = new sdk.MatrixScheduler();
@@ -49,9 +53,7 @@ describe("MatrixClient retrying", function() {
});
it("should mark events as EventStatus.CANCELLED when cancelled", function(done) {
// send a couple of events; the second will be queued
var ev1, ev2;
client.sendMessage(roomId, "m1").then(function(ev) {
expect(ev).toEqual(ev1);
});
@@ -60,10 +62,10 @@ describe("MatrixClient retrying", function() {
});
// both events should be in the timeline at this point
var tl = room.getLiveTimeline().getEvents();
const tl = room.getLiveTimeline().getEvents();
expect(tl.length).toEqual(2);
ev1 = tl[0];
ev2 = tl[1];
const ev1 = tl[0];
const ev2 = tl[1];
expect(ev1.status).toEqual(EventStatus.SENDING);
expect(ev2.status).toEqual(EventStatus.SENDING);
@@ -79,8 +81,9 @@ describe("MatrixClient retrying", function() {
expect(tl.length).toEqual(1);
// shouldn't be able to cancel the first message yet
expect(function() { client.cancelPendingEvent(ev1); })
.toThrow();
expect(function() {
client.cancelPendingEvent(ev1);
}).toThrow();
}).respond(400); // fail the first message
httpBackend.flush().then(function() {
+173 -133
View File
@@ -1,28 +1,32 @@
"use strict";
var sdk = require("../..");
var EventStatus = sdk.EventStatus;
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
import 'source-map-support/register';
const sdk = require("../..");
const EventStatus = sdk.EventStatus;
const HttpBackend = require("../mock-request");
const utils = require("../test-utils");
import expect from 'expect';
describe("MatrixClient room timelines", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var userId = "@alice:localhost";
var userName = "Alice";
var accessToken = "aseukfgwef";
var roomId = "!foo:bar";
var otherUserId = "@bob:localhost";
var USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
const userId = "@alice:localhost";
const userName = "Alice";
const accessToken = "aseukfgwef";
const roomId = "!foo:bar";
const otherUserId = "@bob:localhost";
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName,
});
var ROOM_NAME_EVENT = utils.mkEvent({
const ROOM_NAME_EVENT = utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId,
content: {
name: "Old room name"
}
name: "Old room name",
},
});
var NEXT_SYNC_DATA;
var SYNC_DATA = {
let NEXT_SYNC_DATA;
const SYNC_DATA = {
next_batch: "s_5_3",
rooms: {
join: {
@@ -30,30 +34,30 @@ describe("MatrixClient room timelines", function() {
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello"
})
room: roomId, user: otherUserId, msg: "hello",
}),
],
prev_batch: "f_1_1"
prev_batch: "f_1_1",
},
state: {
events: [
ROOM_NAME_EVENT,
utils.mkMembership({
room: roomId, mship: "join",
user: otherUserId, name: "Bob"
user: otherUserId, name: "Bob",
}),
USER_MEMBERSHIP_EVENT,
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}
}
}
}
creator: userId,
},
}),
],
},
},
},
},
};
function setNextSyncData(events) {
@@ -67,11 +71,11 @@ describe("MatrixClient room timelines", function() {
"!foo:bar": {
timeline: { events: [] },
state: { events: [] },
ephemeral: { events: [] }
}
ephemeral: { events: [] },
},
},
leave: {}
}
leave: {},
},
};
events.forEach(function(e) {
if (e.room_id !== roomId) {
@@ -81,7 +85,7 @@ describe("MatrixClient room timelines", function() {
if (e.__prev_event === undefined) {
throw new Error(
"setNextSyncData needs the prev state set to '__prev_event' " +
"for " + e.type
"for " + e.type,
);
}
if (e.__prev_event !== null) {
@@ -90,18 +94,16 @@ describe("MatrixClient room timelines", function() {
}
// push the current
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
}
else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
} else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
}
else {
} else {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
}
});
}
beforeEach(function(done) {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
@@ -121,7 +123,7 @@ describe("MatrixClient room timelines", function() {
client.startClient();
httpBackend.flush("/pushrules").then(function() {
return httpBackend.flush("/filter");
}).done(done);
}).nodeify(done);
});
afterEach(function() {
@@ -130,12 +132,13 @@ describe("MatrixClient room timelines", function() {
});
describe("local echo events", function() {
it("should be added immediately after calling MatrixClient.sendEvent " +
"with EventStatus.SENDING and the right event.sender", function(done) {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.sendTextMessage(roomId, "I am a fish", "txn1");
@@ -144,7 +147,7 @@ describe("MatrixClient room timelines", function() {
// check status
expect(room.timeline[1].status).toEqual(EventStatus.SENDING);
// check member
var member = room.timeline[1].sender;
const member = room.timeline[1].sender;
expect(member.userId).toEqual(userId);
expect(member.name).toEqual(userName);
@@ -157,21 +160,23 @@ describe("MatrixClient room timelines", function() {
it("should be updated correctly when the send request finishes " +
"BEFORE the event comes down the event stream", function(done) {
var eventId = "$foo:bar";
const eventId = "$foo:bar";
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId
event_id: eventId,
});
var ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId
const ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId,
});
ev.event_id = eventId;
ev.unsigned = {transaction_id: "txn1"};
setNextSyncData([ev]);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
client.sendTextMessage(roomId, "I am a fish", "txn1").done(
function() {
expect(room.timeline[1].getId()).toEqual(eventId);
@@ -187,22 +192,24 @@ describe("MatrixClient room timelines", function() {
it("should be updated correctly when the send request finishes " +
"AFTER the event comes down the event stream", function(done) {
var eventId = "$foo:bar";
const eventId = "$foo:bar";
httpBackend.when("PUT", "/txn1").respond(200, {
event_id: eventId
event_id: eventId,
});
var ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId
const ev = utils.mkMessage({
body: "I am a fish", user: userId, room: roomId,
});
ev.event_id = eventId;
ev.unsigned = {transaction_id: "txn1"};
setNextSyncData([ev]);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
var promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
const promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
httpBackend.flush("/sync", 1).done(function() {
expect(room.timeline.length).toEqual(2);
httpBackend.flush("/txn1", 1);
@@ -212,15 +219,14 @@ describe("MatrixClient room timelines", function() {
done();
});
});
});
httpBackend.flush("/sync", 1);
});
});
describe("paginated events", function() {
var sbEvents;
var sbEndTok = "pagin_end";
let sbEvents;
const sbEndTok = "pagin_end";
beforeEach(function() {
sbEvents = [];
@@ -228,7 +234,7 @@ describe("MatrixClient room timelines", function() {
return {
chunk: sbEvents,
start: "pagin_start",
end: sbEndTok
end: sbEndTok,
};
});
});
@@ -236,13 +242,15 @@ describe("MatrixClient room timelines", function() {
it("should set Room.oldState.paginationToken to null at the start" +
" of the timeline.", function(done) {
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.scrollback(room).done(function() {
expect(room.timeline.length).toEqual(1);
expect(room.oldState.paginationToken).toBeNull();
expect(room.oldState.paginationToken).toBe(null);
done();
});
@@ -263,49 +271,51 @@ describe("MatrixClient room timelines", function() {
// <Bob> hello
// make an m.room.member event for alice's join
var joinMshipEvent = utils.mkMembership({
const joinMshipEvent = utils.mkMembership({
mship: "join", user: userId, room: roomId, name: "Old Alice",
url: null
url: null,
});
// make an m.room.member event with prev_content for alice's nick
// change
var oldMshipEvent = utils.mkMembership({
const oldMshipEvent = utils.mkMembership({
mship: "join", user: userId, room: roomId, name: userName,
url: "mxc://some/url"
url: "mxc://some/url",
});
oldMshipEvent.prev_content = {
displayname: "Old Alice",
avatar_url: null,
membership: "join"
membership: "join",
};
// set the list of events to return on scrollback (/messages)
// N.B. synapse returns /messages in reverse chronological order
sbEvents = [
utils.mkMessage({
user: userId, room: roomId, msg: "I'm alice"
user: userId, room: roomId, msg: "I'm alice",
}),
oldMshipEvent,
utils.mkMessage({
user: userId, room: roomId, msg: "I'm old alice"
user: userId, room: roomId, msg: "I'm old alice",
}),
joinMshipEvent,
];
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
// sync response
expect(room.timeline.length).toEqual(1);
client.scrollback(room).done(function() {
expect(room.timeline.length).toEqual(5);
var joinMsg = room.timeline[0];
const joinMsg = room.timeline[0];
expect(joinMsg.sender.name).toEqual("Old Alice");
var oldMsg = room.timeline[1];
const oldMsg = room.timeline[1];
expect(oldMsg.sender.name).toEqual("Old Alice");
var newMsg = room.timeline[3];
const newMsg = room.timeline[3];
expect(newMsg.sender.name).toEqual(userName);
done();
});
@@ -320,16 +330,18 @@ describe("MatrixClient room timelines", function() {
// set the list of events to return on scrollback
sbEvents = [
utils.mkMessage({
user: userId, room: roomId, msg: "I am new"
user: userId, room: roomId, msg: "I am new",
}),
utils.mkMessage({
user: userId, room: roomId, msg: "I am old"
})
user: userId, room: roomId, msg: "I am old",
}),
];
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.scrollback(room).done(function() {
@@ -349,14 +361,16 @@ describe("MatrixClient room timelines", function() {
// set the list of events to return on scrollback
sbEvents = [
utils.mkMessage({
user: userId, room: roomId, msg: "I am new"
})
user: userId, room: roomId, msg: "I am new",
}),
];
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
expect(room.oldState.paginationToken).toBeDefined();
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
expect(room.oldState.paginationToken).toBeTruthy();
client.scrollback(room, 1).done(function() {
expect(room.oldState.paginationToken).toEqual(sbEndTok);
@@ -373,17 +387,19 @@ describe("MatrixClient room timelines", function() {
describe("new events", function() {
it("should be added to the right place in the timeline", function(done) {
var eventData = [
const eventData = [
utils.mkMessage({user: userId, room: roomId}),
utils.mkMessage({user: userId, room: roomId}),
utils.mkMessage({user: userId, room: roomId})
];
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
var index = 0;
let index = 0;
client.on("Room.timeline", function(event, rm, toStart) {
expect(toStart).toBe(false);
expect(rm).toEqual(room);
@@ -393,13 +409,15 @@ describe("MatrixClient room timelines", function() {
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).then(function() {
return utils.syncPromise(client);
}).then(function() {
expect(index).toEqual(2);
expect(room.timeline.length).toEqual(3);
expect(room.timeline[2].event).toEqual(
eventData[1]
eventData[1],
);
expect(room.timeline[1].event).toEqual(
eventData[0]
eventData[0],
);
}).catch(utils.failTest).done(done);
});
@@ -407,22 +425,26 @@ describe("MatrixClient room timelines", function() {
});
it("should set the right event.sender values", function(done) {
var eventData = [
const eventData = [
utils.mkMessage({user: userId, room: roomId}),
utils.mkMembership({
user: userId, room: roomId, mship: "join", name: "New Name"
user: userId, room: roomId, mship: "join", name: "New Name",
}),
utils.mkMessage({user: userId, room: roomId})
utils.mkMessage({user: userId, room: roomId}),
];
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
httpBackend.flush("/sync", 1).then(function() {
var preNameEvent = room.timeline[room.timeline.length - 3];
var postNameEvent = room.timeline[room.timeline.length - 1];
return utils.syncPromise(client);
}).then(function() {
const preNameEvent = room.timeline[room.timeline.length - 3];
const postNameEvent = room.timeline[room.timeline.length - 1];
expect(preNameEvent.sender.name).toEqual(userName);
expect(postNameEvent.sender.name).toEqual("New Name");
}).catch(utils.failTest).done(done);
@@ -431,35 +453,41 @@ describe("MatrixClient room timelines", function() {
});
it("should set the right room.name", function(done) {
var secondRoomNameEvent = utils.mkEvent({
const secondRoomNameEvent = utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 2"
}
name: "Room 2",
},
});
secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
setNextSyncData([secondRoomNameEvent]);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
var nameEmitCount = 0;
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
let nameEmitCount = 0;
client.on("Room.name", function(rm) {
nameEmitCount += 1;
});
httpBackend.flush("/sync", 1).done(function() {
httpBackend.flush("/sync", 1).then(() => {
return utils.syncPromise(client);
}).done(function() {
expect(nameEmitCount).toEqual(1);
expect(room.name).toEqual("Room 2");
// do another round
var thirdRoomNameEvent = utils.mkEvent({
const thirdRoomNameEvent = utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 3"
}
name: "Room 3",
},
});
thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
setNextSyncData([thirdRoomNameEvent]);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend.flush("/sync", 1).done(function() {
httpBackend.flush("/sync", 1).then(() => {
return utils.syncPromise(client);
}).done(function() {
expect(nameEmitCount).toEqual(2);
expect(room.name).toEqual("Room 3");
done();
@@ -470,32 +498,36 @@ describe("MatrixClient room timelines", function() {
});
it("should set the right room members", function(done) {
var userC = "@cee:bar";
var userD = "@dee:bar";
var eventData = [
const userC = "@cee:bar";
const userD = "@dee:bar";
const eventData = [
utils.mkMembership({
user: userC, room: roomId, mship: "join", name: "C"
user: userC, room: roomId, mship: "join", name: "C",
}),
utils.mkMembership({
user: userC, room: roomId, mship: "invite", skey: userD
})
user: userC, room: roomId, mship: "invite", skey: userD,
}),
];
eventData[0].__prev_event = null;
eventData[1].__prev_event = null;
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
httpBackend.flush("/sync", 1).then(function() {
return utils.syncPromise(client);
}).then(function() {
expect(room.currentState.getMembers().length).toEqual(4);
expect(room.currentState.getMember(userC).name).toEqual("C");
expect(room.currentState.getMember(userC).membership).toEqual(
"join"
"join",
);
expect(room.currentState.getMember(userD).name).toEqual(userD);
expect(room.currentState.getMember(userD).membership).toEqual(
"invite"
"invite",
);
}).catch(utils.failTest).done(done);
});
@@ -505,28 +537,32 @@ describe("MatrixClient room timelines", function() {
describe("gappy sync", function() {
it("should copy the last known state to the new timeline", function(done) {
var eventData = [
const eventData = [
utils.mkMessage({user: userId, room: roomId}),
];
setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
return utils.syncPromise(client);
}).done(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(eventData[0]);
expect(room.currentState.getMembers().length).toEqual(2);
expect(room.currentState.getMember(userId).name).toEqual(userName);
expect(room.currentState.getMember(userId).membership).toEqual(
"join"
"join",
);
expect(room.currentState.getMember(otherUserId).name).toEqual("Bob");
expect(room.currentState.getMember(otherUserId).membership).toEqual(
"join"
"join",
);
done();
});
@@ -535,24 +571,28 @@ describe("MatrixClient room timelines", function() {
});
it("should emit a 'Room.timelineReset' event", function(done) {
var eventData = [
const eventData = [
utils.mkMessage({user: userId, room: roomId}),
];
setNextSyncData(eventData);
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
client.on("sync", function(state) {
if (state !== "PREPARED") { return; }
var room = client.getRoom(roomId);
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
var emitCount = 0;
let emitCount = 0;
client.on("Room.timelineReset", function(emitRoom) {
expect(emitRoom).toEqual(room);
emitCount++;
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
return utils.syncPromise(client);
}).done(function() {
expect(emitCount).toEqual(1);
done();
});
+160 -157
View File
@@ -1,30 +1,34 @@
"use strict";
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
var MatrixEvent = sdk.MatrixEvent;
var EventTimeline = sdk.EventTimeline;
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const utils = require("../test-utils");
const MatrixEvent = sdk.MatrixEvent;
const EventTimeline = sdk.EventTimeline;
import expect from 'expect';
describe("MatrixClient syncing", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var selfUserId = "@alice:localhost";
var selfAccessToken = "aseukfgwef";
var otherUserId = "@bob:localhost";
var userA = "@alice:bar";
var userB = "@bob:bar";
var userC = "@claire:bar";
var roomOne = "!foo:localhost";
var roomTwo = "!bar:localhost";
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
const otherUserId = "@bob:localhost";
const userA = "@alice:bar";
const userB = "@bob:bar";
const userC = "@claire:bar";
const roomOne = "!foo:localhost";
const roomTwo = "!bar:localhost";
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: selfUserId,
accessToken: selfAccessToken
accessToken: selfAccessToken,
});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
@@ -36,10 +40,10 @@ describe("MatrixClient syncing", function() {
});
describe("startClient", function() {
var syncData = {
const syncData = {
next_batch: "batch_token",
rooms: {},
presence: {}
presence: {},
};
it("should /sync after /pushrules and /filter.", function(done) {
@@ -68,17 +72,16 @@ describe("MatrixClient syncing", function() {
});
describe("resolving invites to profile info", function() {
var syncData = {
const syncData = {
next_batch: "s_5_3",
presence: {
events: []
events: [],
},
rooms: {
join: {
}
}
},
},
};
beforeEach(function() {
@@ -87,54 +90,54 @@ describe("MatrixClient syncing", function() {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
})
]
room: roomOne, user: otherUserId, msg: "hello",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
creator: selfUserId,
},
}),
],
},
};
});
it("should resolve incoming invites from /sync", function(done) {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
room: roomOne, mship: "invite", user: userC,
}),
);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/profile/" + encodeURIComponent(userC)).respond(
200, {
avatar_url: "mxc://flibble/wibble",
displayname: "The Boss"
}
displayname: "The Boss",
},
);
client.startClient({
resolveInvitesToProfiles: true
resolveInvitesToProfiles: true,
});
httpBackend.flush().done(function() {
var member = client.getRoom(roomOne).getMember(userC);
const member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual("The Boss");
expect(
member.getAvatarUrl("home.server.url", null, null, null, false)
).toBeDefined();
member.getAvatarUrl("home.server.url", null, null, null, false),
).toBeTruthy();
done();
});
});
@@ -142,23 +145,23 @@ describe("MatrixClient syncing", function() {
it("should use cached values from m.presence wherever possible", function(done) {
syncData.presence.events = [
utils.mkPresence({
user: userC, presence: "online", name: "The Ghost"
user: userC, presence: "online", name: "The Ghost",
}),
];
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
room: roomOne, mship: "invite", user: userC,
}),
);
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient({
resolveInvitesToProfiles: true
resolveInvitesToProfiles: true,
});
httpBackend.flush().done(function() {
var member = client.getRoom(roomOne).getMember(userC);
const member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual("The Ghost");
done();
});
@@ -167,18 +170,18 @@ describe("MatrixClient syncing", function() {
it("should result in events on the room member firing", function(done) {
syncData.presence.events = [
utils.mkPresence({
user: userC, presence: "online", name: "The Ghost"
})
user: userC, presence: "online", name: "The Ghost",
}),
];
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
room: roomOne, mship: "invite", user: userC,
}),
);
httpBackend.when("GET", "/sync").respond(200, syncData);
var latestFiredName = null;
let latestFiredName = null;
client.on("RoomMember.name", function(event, m) {
if (m.userId === userC && m.roomId === roomOne) {
latestFiredName = m.name;
@@ -186,7 +189,7 @@ describe("MatrixClient syncing", function() {
});
client.startClient({
resolveInvitesToProfiles: true
resolveInvitesToProfiles: true,
});
httpBackend.flush().done(function() {
@@ -198,8 +201,8 @@ describe("MatrixClient syncing", function() {
it("should no-op if resolveInvitesToProfiles is not set", function(done) {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC
})
room: roomOne, mship: "invite", user: userC,
}),
);
httpBackend.when("GET", "/sync").respond(200, syncData);
@@ -207,29 +210,29 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flush().done(function() {
var member = client.getRoom(roomOne).getMember(userC);
const member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual(userC);
expect(
member.getAvatarUrl("home.server.url", null, null, null, false)
).toBeNull();
member.getAvatarUrl("home.server.url", null, null, null, false),
).toBe(null);
done();
});
});
});
describe("users", function() {
var syncData = {
const syncData = {
next_batch: "nb",
presence: {
events: [
utils.mkPresence({
user: userA, presence: "online"
user: userA, presence: "online",
}),
utils.mkPresence({
user: userB, presence: "unavailable"
})
]
}
user: userB, presence: "unavailable",
}),
],
},
};
it("should create users for presence events from /sync",
@@ -247,80 +250,80 @@ describe("MatrixClient syncing", function() {
});
describe("room state", function() {
var msgText = "some text here";
var otherDisplayName = "Bob Smith";
const msgText = "some text here";
const otherDisplayName = "Bob Smith";
var syncData = {
const syncData = {
rooms: {
join: {
}
}
},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
})
]
room: roomOne, user: otherUserId, msg: "hello",
}),
],
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name"
}
name: "Old room name",
},
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
creator: selfUserId,
},
}),
],
},
};
syncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hiii"
})
]
room: roomTwo, user: otherUserId, msg: "hiii",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomTwo, mship: "join", user: otherUserId,
name: otherDisplayName
name: otherDisplayName,
}),
utils.mkMembership({
room: roomTwo, mship: "join", user: selfUserId
room: roomTwo, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomTwo, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
creator: selfUserId,
},
}),
],
},
};
var nextSyncData = {
const nextSyncData = {
rooms: {
join: {
}
}
},
},
};
nextSyncData.rooms.join[roomOne] = {
@@ -328,28 +331,28 @@ describe("MatrixClient syncing", function() {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: selfUserId,
content: { name: "A new room name" }
})
]
}
content: { name: "A new room name" },
}),
],
},
};
nextSyncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: msgText
})
]
room: roomTwo, user: otherUserId, msg: msgText,
}),
],
},
ephemeral: {
events: [
utils.mkEvent({
type: "m.typing", room: roomTwo,
content: { user_ids: [otherUserId] }
})
]
}
content: { user_ids: [otherUserId] },
}),
],
},
};
it("should continually recalculate the right room name.", function(done) {
@@ -359,10 +362,10 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomOne);
const room = client.getRoom(roomOne);
// should have clobbered the name to the one from /events
expect(room.name).toEqual(
nextSyncData.rooms.join[roomOne].state.events[0].content.name
nextSyncData.rooms.join[roomOne].state.events[0].content.name,
);
done();
});
@@ -375,7 +378,7 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomTwo);
const room = client.getRoom(roomTwo);
// should have added the message from /events
expect(room.timeline.length).toEqual(2);
expect(room.timeline[1].getContent().body).toEqual(msgText);
@@ -389,7 +392,7 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomTwo);
const room = client.getRoom(roomTwo);
// should use the display name of the other person.
expect(room.name).toEqual(otherDisplayName);
done();
@@ -403,12 +406,12 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomTwo);
var member = room.getMember(otherUserId);
expect(member).toBeDefined();
const room = client.getRoom(roomTwo);
let member = room.getMember(otherUserId);
expect(member).toBeTruthy();
expect(member.typing).toEqual(true);
member = room.getMember(selfUserId);
expect(member).toBeDefined();
expect(member).toBeTruthy();
expect(member.typing).toEqual(false);
done();
});
@@ -425,7 +428,7 @@ describe("MatrixClient syncing", function() {
describe("timeline", function() {
beforeEach(function() {
var syncData = {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
@@ -435,7 +438,7 @@ describe("MatrixClient syncing", function() {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
room: roomOne, user: otherUserId, msg: "hello",
}),
],
prev_batch: "pagTok",
@@ -449,7 +452,7 @@ describe("MatrixClient syncing", function() {
});
it("should set the back-pagination token on new rooms", function(done) {
var syncData = {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
@@ -459,7 +462,7 @@ describe("MatrixClient syncing", function() {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "roomtwo"
room: roomTwo, user: otherUserId, msg: "roomtwo",
}),
],
prev_batch: "roomtwotok",
@@ -469,8 +472,8 @@ describe("MatrixClient syncing", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.flush().then(function() {
var room = client.getRoom(roomTwo);
var tok = room.getLiveTimeline()
const room = client.getRoom(roomTwo);
const tok = room.getLiveTimeline()
.getPaginationToken(EventTimeline.BACKWARDS);
expect(tok).toEqual("roomtwotok");
done();
@@ -478,7 +481,7 @@ describe("MatrixClient syncing", function() {
});
it("should set the back-pagination token on gappy syncs", function(done) {
var syncData = {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
@@ -488,7 +491,7 @@ describe("MatrixClient syncing", function() {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world"
room: roomOne, user: otherUserId, msg: "world",
}),
],
limited: true,
@@ -497,20 +500,20 @@ describe("MatrixClient syncing", function() {
};
httpBackend.when("GET", "/sync").respond(200, syncData);
var resetCallCount = 0;
let resetCallCount = 0;
// the token should be set *before* timelineReset is emitted
client.on("Room.timelineReset", function(room) {
resetCallCount++;
var tl = room.getLiveTimeline();
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(0);
var tok = tl.getPaginationToken(EventTimeline.BACKWARDS);
const tok = tl.getPaginationToken(EventTimeline.BACKWARDS);
expect(tok).toEqual("newerTok");
});
httpBackend.flush().then(function() {
var room = client.getRoom(roomOne);
var tl = room.getLiveTimeline();
const room = client.getRoom(roomOne);
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(1);
expect(resetCallCount).toEqual(1);
done();
@@ -519,80 +522,80 @@ describe("MatrixClient syncing", function() {
});
describe("receipts", function() {
var syncData = {
const syncData = {
rooms: {
join: {
}
}
},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
room: roomOne, user: otherUserId, msg: "hello",
}),
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world"
})
]
room: roomOne, user: otherUserId, msg: "world",
}),
],
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name"
}
name: "Old room name",
},
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
creator: selfUserId,
},
}),
],
},
};
beforeEach(function() {
syncData.rooms.join[roomOne].ephemeral = {
events: []
events: [],
};
});
it("should sync receipts from /sync.", function(done) {
var ackEvent = syncData.rooms.join[roomOne].timeline.events[0];
var receipt = {};
const ackEvent = syncData.rooms.join[roomOne].timeline.events[0];
const receipt = {};
receipt[ackEvent.event_id] = {
"m.read": {}
"m.read": {},
};
receipt[ackEvent.event_id]["m.read"][userC] = {
ts: 176592842636
ts: 176592842636,
};
syncData.rooms.join[roomOne].ephemeral.events = [{
content: receipt,
room_id: roomOne,
type: "m.receipt"
type: "m.receipt",
}];
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomOne);
const room = client.getRoom(roomOne);
expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{
type: "m.read",
userId: userC,
data: {
ts: 176592842636
}
ts: 176592842636,
},
}]);
done();
});
@@ -647,10 +650,10 @@ describe("MatrixClient syncing", function() {
});
it("should set the back-pagination token on left rooms", function(done) {
var syncData = {
const syncData = {
next_batch: "batch_token",
rooms: {
leave: {}
leave: {},
},
};
@@ -658,7 +661,7 @@ describe("MatrixClient syncing", function() {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hello"
room: roomTwo, user: otherUserId, msg: "hello",
}),
],
prev_batch: "pagTok",
@@ -666,14 +669,14 @@ describe("MatrixClient syncing", function() {
};
httpBackend.when("POST", "/filter").respond(200, {
filter_id: "another_id"
filter_id: "another_id",
});
httpBackend.when("GET", "/sync").respond(200, syncData);
client.syncLeftRooms().then(function() {
var room = client.getRoom(roomTwo);
var tok = room.getLiveTimeline().getPaginationToken(
const room = client.getRoom(roomTwo);
const tok = room.getLiveTimeline().getPaginationToken(
EventTimeline.BACKWARDS);
expect(tok).toEqual("pagTok");
+901 -349
View File
File diff suppressed because it is too large Load Diff
+71 -50
View File
@@ -1,5 +1,6 @@
"use strict";
var q = require("q");
const q = require("q");
import expect from 'expect';
/**
* Construct a mock HTTP backend, heavily inspired by Angular.js.
@@ -8,15 +9,15 @@ var q = require("q");
function HttpBackend() {
this.requests = [];
this.expectedRequests = [];
var self = this;
const self = this;
// the request function dependency that the SDK needs.
this.requestFn = function(opts, callback) {
var req = new Request(opts, callback);
const req = new Request(opts, callback);
console.log("HTTP backend received request: %s", req);
self.requests.push(req);
var abort = function() {
var idx = self.requests.indexOf(req);
const abort = function() {
const idx = self.requests.indexOf(req);
if (idx >= 0) {
console.log("Aborting HTTP request: %s %s", opts.method,
opts.uri);
@@ -26,7 +27,7 @@ function HttpBackend() {
};
return {
abort: abort
abort: abort,
};
};
}
@@ -35,43 +36,48 @@ HttpBackend.prototype = {
* Respond to all of the requests (flush the queue).
* @param {string} path The path to flush (optional) default: all.
* @param {integer} numToFlush The number of things to flush (optional), default: all.
* @return {Promise} resolved when there is nothing left to flush.
* @param {integer=} waitTime The time (in ms) to wait for a request to happen.
* default: 5
*
* @return {Promise} resolves when there is nothing left to flush, with the
* number of requests flushed
*/
flush: function(path, numToFlush) {
var defer = q.defer();
var self = this;
var flushed = 0;
var triedWaiting = false;
flush: function(path, numToFlush, waitTime) {
const defer = q.defer();
const self = this;
let flushed = 0;
let triedWaiting = false;
if (waitTime === undefined) {
waitTime = 5;
}
console.log(
"HTTP backend flushing... (path=%s numToFlush=%s)", path, numToFlush
"HTTP backend flushing... (path=%s numToFlush=%s waitTime=%s)",
path, numToFlush, waitTime,
);
var tryFlush = function() {
const tryFlush = function() {
// if there's more real requests and more expected requests, flush 'em.
console.log(
" trying to flush queue => reqs=%s expected=%s [%s]",
self.requests.length, self.expectedRequests.length, path
" trying to flush queue => reqs=[%s] expected=[%s]",
self.requests, self.expectedRequests.map((er) => er.path),
);
if (self._takeFromQueue(path)) {
// try again on the next tick.
console.log(" flushed. Trying for more. [%s]", path);
flushed += 1;
if (numToFlush && flushed === numToFlush) {
console.log(" [%s] Flushed assigned amount: %s", path, numToFlush);
defer.resolve();
}
else {
console.log(" Flushed assigned amount: %s", numToFlush);
defer.resolve(flushed);
} else {
console.log(" flushed. Trying for more.");
setTimeout(tryFlush, 0);
}
}
else if (flushed === 0 && !triedWaiting) {
} else if (flushed === 0 && !triedWaiting) {
// we may not have made the request yet, wait a generous amount of
// time before giving up.
setTimeout(tryFlush, 5);
setTimeout(tryFlush, waitTime);
triedWaiting = true;
}
else {
console.log(" no more flushes. [%s]", path);
defer.resolve();
} else {
console.log(" no more flushes.");
defer.resolve(flushed);
}
};
@@ -86,14 +92,19 @@ HttpBackend.prototype = {
* @return {boolean} true if something was resolved.
*/
_takeFromQueue: function(path) {
var req = null;
var i, j;
var matchingReq, expectedReq, testResponse = null;
let req = null;
let i;
let j;
let matchingReq = null;
let expectedReq = null;
let testResponse = null;
for (i = 0; i < this.requests.length; i++) {
req = this.requests[i];
for (j = 0; j < this.expectedRequests.length; j++) {
expectedReq = this.expectedRequests[j];
if (path && path !== expectedReq.path) { continue; }
if (path && path !== expectedReq.path) {
continue;
}
if (expectedReq.method === req.method &&
req.path.indexOf(expectedReq.path) !== -1) {
if (!expectedReq.data || (JSON.stringify(expectedReq.data) ===
@@ -115,12 +126,12 @@ HttpBackend.prototype = {
}
testResponse = matchingReq.response;
console.log(" responding to %s", matchingReq.path);
var body = testResponse.body;
let body = testResponse.body;
if (Object.prototype.toString.call(body) == "[object Function]") {
body = body(req.path, req.data);
}
req.callback(
testResponse.err, testResponse.response, body
testResponse.err, testResponse.response, body,
);
matchingReq = null;
}
@@ -135,10 +146,10 @@ HttpBackend.prototype = {
* Makes sure that the SDK hasn't sent any more requests to the backend.
*/
verifyNoOutstandingRequests: function() {
var firstOutstandingReq = this.requests[0] || {};
const firstOutstandingReq = this.requests[0] || {};
expect(this.requests.length).toEqual(0,
"Expected no more HTTP requests but received request to " +
firstOutstandingReq.path
firstOutstandingReq.path,
);
},
@@ -146,9 +157,9 @@ HttpBackend.prototype = {
* Makes sure that the test doesn't have any unresolved requests.
*/
verifyNoOutstandingExpectation: function() {
var firstOutstandingExpectation = this.expectedRequests[0] || {};
const firstOutstandingExpectation = this.expectedRequests[0] || {};
expect(this.expectedRequests.length).toEqual(0,
"Expected to see HTTP request for " + firstOutstandingExpectation.path
"Expected to see HTTP request for " + firstOutstandingExpectation.path,
);
},
@@ -160,10 +171,10 @@ HttpBackend.prototype = {
* @return {Request} An expected request.
*/
when: function(method, path, data) {
var pendingReq = new ExpectedRequest(method, path, data);
const pendingReq = new ExpectedRequest(method, path, data);
this.expectedRequests.push(pendingReq);
return pendingReq;
}
},
};
/**
@@ -206,10 +217,10 @@ ExpectedRequest.prototype = {
this.response = {
response: {
statusCode: code,
headers: {}
headers: {},
},
body: data,
err: null
err: null,
};
},
@@ -222,12 +233,12 @@ ExpectedRequest.prototype = {
this.response = {
response: {
statusCode: code,
headers: {}
headers: {},
},
body: null,
err: err
err: err,
};
}
},
};
/**
@@ -242,23 +253,33 @@ function Request(opts, callback) {
this.callback = callback;
Object.defineProperty(this, 'method', {
get: function() { return opts.method; }
get: function() {
return opts.method;
},
});
Object.defineProperty(this, 'path', {
get: function() { return opts.uri; }
get: function() {
return opts.uri;
},
});
Object.defineProperty(this, 'data', {
get: function() { return opts.body; }
get: function() {
return opts.body;
},
});
Object.defineProperty(this, 'queryParams', {
get: function() { return opts.qs; }
get: function() {
return opts.qs;
},
});
Object.defineProperty(this, 'headers', {
get: function() { return opts.headers || {}; }
get: function() {
return opts.headers || {};
},
});
}
+56 -25
View File
@@ -1,14 +1,37 @@
"use strict";
var sdk = require("..");
var MatrixEvent = sdk.MatrixEvent;
import expect from 'expect';
import q from 'q';
const sdk = require("..");
const MatrixEvent = sdk.MatrixEvent;
/**
* Return a promise that is resolved when the client next emits a
* SYNCING event.
* @param {Object} client The client
* @return {Promise} Resolves once the client has emitted a SYNCING event
*/
module.exports.syncPromise = function(client) {
const def = q.defer();
const cb = (state) => {
if (state == 'SYNCING') {
def.resolve();
} else {
client.once('sync', cb);
}
};
client.once('sync', cb);
return def.promise;
};
/**
* Perform common actions before each test case, e.g. printing the test case
* name to stdout.
* @param {TestCase} testCase The test case that is about to be run.
* @param {Mocha.Context} context The test context
*/
module.exports.beforeEach = function(testCase) {
var desc = testCase.suite.description + " : " + testCase.description;
module.exports.beforeEach = function(context) {
const desc = context.currentTest.fullTitle();
console.log(desc);
console.log(new Array(1 + desc.length).join("="));
};
@@ -20,21 +43,20 @@ module.exports.beforeEach = function(testCase) {
* @return {Object} An instantiated object with spied methods/properties.
*/
module.exports.mock = function(constr, name) {
// By Tim Buschtöns
// Based on
// http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
var HelperConstr = new Function(); // jshint ignore:line
const HelperConstr = new Function(); // jshint ignore:line
HelperConstr.prototype = constr.prototype;
var result = new HelperConstr();
result.jasmineToString = function() {
const result = new HelperConstr();
result.toString = function() {
return "mock" + (name ? " of " + name : "");
};
for (var key in constr.prototype) { // jshint ignore:line
for (const key in constr.prototype) { // eslint-disable-line guard-for-in
try {
if (constr.prototype[key] instanceof Function) {
result[key] = jasmine.createSpy((name || "mock") + '.' + key);
result[key] = expect.createSpy();
}
}
catch (ex) {
} catch (ex) {
// Direct access to some non-function fields of DOM prototypes may
// cause exceptions.
// Overwriting will not work either in that case.
@@ -58,17 +80,16 @@ module.exports.mkEvent = function(opts) {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
var event = {
const event = {
type: opts.type,
room_id: opts.room,
sender: opts.sender || opts.user, // opts.user for backwards-compat
content: opts.content,
event_id: "$" + Math.random() + "-" + Math.random()
event_id: "$" + Math.random() + "-" + Math.random(),
};
if (opts.skey !== undefined) {
event.state_key = opts.skey;
}
else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
"m.room.power_levels", "m.room.topic",
"com.example.state"].indexOf(opts.type) !== -1) {
event.state_key = "";
@@ -85,7 +106,7 @@ module.exports.mkPresence = function(opts) {
if (!opts.user) {
throw new Error("Missing user");
}
var event = {
const event = {
event_id: "$" + Math.random() + "-" + Math.random(),
type: "m.presence",
sender: opts.sender || opts.user, // opts.user for backwards-compat
@@ -93,8 +114,8 @@ module.exports.mkPresence = function(opts) {
avatar_url: opts.url,
displayname: opts.name,
last_active_ago: opts.ago,
presence: opts.presence || "offline"
}
presence: opts.presence || "offline",
},
};
return opts.event ? new MatrixEvent(event) : event;
};
@@ -121,10 +142,14 @@ module.exports.mkMembership = function(opts) {
throw new Error("Missing .mship => " + JSON.stringify(opts));
}
opts.content = {
membership: opts.mship
membership: opts.mship,
};
if (opts.name) { opts.content.displayname = opts.name; }
if (opts.url) { opts.content.avatar_url = opts.url; }
if (opts.name) {
opts.content.displayname = opts.name;
}
if (opts.url) {
opts.content.avatar_url = opts.url;
}
return module.exports.mkEvent(opts);
};
@@ -147,7 +172,7 @@ module.exports.mkMessage = function(opts) {
}
opts.content = {
msgtype: "m.text",
body: opts.msg
body: opts.msg,
};
return module.exports.mkEvent(opts);
};
@@ -191,6 +216,12 @@ module.exports.MockStorageApi = function() {
this.data = {};
};
module.exports.MockStorageApi.prototype = {
get length() {
return Object.keys(this.data).length;
},
key: function(i) {
return Object.keys(this.data)[i];
},
setItem: function(k, v) {
this.data[k] = v;
},
@@ -199,5 +230,5 @@ module.exports.MockStorageApi.prototype = {
},
removeItem: function(k) {
delete this.data[k];
}
},
};
+22 -19
View File
@@ -1,34 +1,37 @@
"use strict";
var ContentRepo = require("../../lib/content-repo");
var testUtils = require("../test-utils");
import 'source-map-support/register';
const ContentRepo = require("../../lib/content-repo");
const testUtils = require("../test-utils");
import expect from 'expect';
describe("ContentRepo", function() {
var baseUrl = "https://my.home.server";
const baseUrl = "https://my.home.server";
beforeEach(function() {
testUtils.beforeEach(this);
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe("getHttpUriForMxc", function() {
it("should do nothing to HTTP URLs when allowing direct links", function() {
var httpUrl = "http://example.com/image.jpeg";
const httpUrl = "http://example.com/image.jpeg";
expect(
ContentRepo.getHttpUriForMxc(
baseUrl, httpUrl, undefined, undefined, undefined, true
)
baseUrl, httpUrl, undefined, undefined, undefined, true,
),
).toEqual(httpUrl);
});
it("should return the empty string HTTP URLs by default", function() {
var httpUrl = "http://example.com/image.jpeg";
const httpUrl = "http://example.com/image.jpeg";
expect(ContentRepo.getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
});
it("should return a download URL if no width/height/resize are specified",
function() {
var mxcUri = "mxc://server.name/resourceid";
const mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v1/download/server.name/resourceid"
baseUrl + "/_matrix/media/v1/download/server.name/resourceid",
);
});
@@ -38,27 +41,27 @@ describe("ContentRepo", function() {
it("should return a thumbnail URL if a width/height/resize is specified",
function() {
var mxcUri = "mxc://server.name/resourceid";
const mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
"?width=32&height=64&method=crop"
"?width=32&height=64&method=crop",
);
});
it("should put fragments from mxc:// URIs after any query parameters",
function() {
var mxcUri = "mxc://server.name/resourceid#automade";
const mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
"?width=32#automade"
"?width=32#automade",
);
});
it("should put fragments from mxc:// URIs at the end of the HTTP URI",
function() {
var mxcUri = "mxc://server.name/resourceid#automade";
const mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v1/download/server.name/resourceid#automade"
baseUrl + "/_matrix/media/v1/download/server.name/resourceid#automade",
);
});
});
@@ -71,21 +74,21 @@ describe("ContentRepo", function() {
it("should set w/h by default to 96", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar")).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foobar" +
"?width=96&height=96"
"?width=96&height=96",
);
});
it("should be able to set custom w/h", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foobar" +
"?width=32&height=64"
"?width=32&height=64",
);
});
it("should URL encode the identicon string", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foo%23bar" +
"?width=32&height=64"
"?width=32&height=64",
);
});
});
+10 -4
View File
@@ -1,14 +1,20 @@
"use strict";
var Crypto = require("../../lib/crypto");
var sdk = require("../..");
import 'source-map-support/register';
const sdk = require("../..");
let Crypto;
if (sdk.CRYPTO_ENABLED) {
Crypto = require("../../lib/crypto");
}
import expect from 'expect';
describe("Crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
it("Crypto exposes the correct olm library version", function() {
expect(Crypto.getOlmVersion()).toEqual([1, 3, 0]);
expect(Crypto.getOlmVersion()[0]).toEqual(2);
});
});
+73 -65
View File
@@ -1,32 +1,37 @@
"use strict";
var sdk = require("../..");
var EventTimeline = sdk.EventTimeline;
var utils = require("../test-utils");
import 'source-map-support/register';
const sdk = require("../..");
const EventTimeline = sdk.EventTimeline;
const utils = require("../test-utils");
function mockRoomStates(timeline) {
timeline._startState = utils.mock(sdk.RoomState, "startState");
timeline._endState = utils.mock(sdk.RoomState, "endState");
}
import expect from 'expect';
describe("EventTimeline", function() {
var roomId = "!foo:bar";
var userA = "@alice:bar";
var userB = "@bertha:bar";
var timeline;
const roomId = "!foo:bar";
const userA = "@alice:bar";
const userB = "@bertha:bar";
let timeline;
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
// XXX: this is a horrid hack; should use sinon or something instead to mock
var timelineSet = { room: { roomId: roomId }};
timelineSet.room.getUnfilteredTimelineSet = function() { return timelineSet; };
const timelineSet = { room: { roomId: roomId }};
timelineSet.room.getUnfilteredTimelineSet = function() {
return timelineSet;
};
timeline = new EventTimeline(timelineSet);
});
describe("construction", function() {
it("getRoomId should get room id", function() {
var v = timeline.getRoomId();
const v = timeline.getRoomId();
expect(v).toEqual(roomId);
});
});
@@ -37,7 +42,7 @@ describe("EventTimeline", function() {
});
it("should copy state events to start and end state", function() {
var events = [
const events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA,
event: true,
@@ -46,34 +51,38 @@ describe("EventTimeline", function() {
type: "m.room.name", room: roomId, user: userB,
event: true,
content: { name: "New room" },
})
}),
];
timeline.initialiseState(events);
expect(timeline._startState.setStateEvents).toHaveBeenCalledWith(
events
events,
);
expect(timeline._endState.setStateEvents).toHaveBeenCalledWith(
events
events,
);
});
it("should raise an exception if called after events are added", function() {
var event =
const event =
utils.mkMessage({
room: roomId, user: userA, msg: "Adam stole the plushies",
event: true,
});
var state = [
const state = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA,
event: true,
})
}),
];
expect(function() { timeline.initialiseState(state); }).not.toThrow();
expect(function() {
timeline.initialiseState(state);
}).toNotThrow();
timeline.addEvent(event, false);
expect(function() { timeline.initialiseState(state); }).toThrow();
expect(function() {
timeline.initialiseState(state);
}).toThrow();
});
});
@@ -99,8 +108,8 @@ describe("EventTimeline", function() {
});
it("setNeighbouringTimeline should set neighbour", function() {
var prev = {a: "a"};
var next = {b: "b"};
const prev = {a: "a"};
const next = {b: "b"};
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(prev);
@@ -108,11 +117,11 @@ describe("EventTimeline", function() {
});
it("setNeighbouringTimeline should throw if called twice", function() {
var prev = {a: "a"};
var next = {b: "b"};
const prev = {a: "a"};
const next = {b: "b"};
expect(function() {
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
}).not.toThrow();
}).toNotThrow();
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS))
.toBe(prev);
expect(function() {
@@ -121,7 +130,7 @@ describe("EventTimeline", function() {
expect(function() {
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
}).not.toThrow();
}).toNotThrow();
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS))
.toBe(next);
expect(function() {
@@ -135,7 +144,7 @@ describe("EventTimeline", function() {
mockRoomStates(timeline);
});
var events = [
const events = [
utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry",
event: true,
@@ -148,7 +157,7 @@ describe("EventTimeline", function() {
it("should be able to add events to the end", function() {
timeline.addEvent(events[0], false);
var initialIndex = timeline.getBaseIndex();
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], false);
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
@@ -158,7 +167,7 @@ describe("EventTimeline", function() {
it("should be able to add events to the start", function() {
timeline.addEvent(events[0], true);
var initialIndex = timeline.getBaseIndex();
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], true);
expect(timeline.getBaseIndex()).toEqual(initialIndex + 1);
expect(timeline.getEvents().length).toEqual(2);
@@ -167,38 +176,38 @@ describe("EventTimeline", function() {
});
it("should set event.sender for new and old events", function() {
var sentinel = {
const sentinel = {
userId: userA,
membership: "join",
name: "Alice"
name: "Alice",
};
var oldSentinel = {
const oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
name: "Old Alice",
};
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
.andCallFake(function(uid) {
.andCall(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
.andCallFake(function(uid) {
.andCall(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkEvent({
const newEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" }
content: { name: "New Room Name" },
});
var oldEv = utils.mkEvent({
const oldEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "Old Room Name" }
content: { name: "Old Room Name" },
});
timeline.addEvent(newEv, false);
@@ -209,36 +218,36 @@ describe("EventTimeline", function() {
it("should set event.target for new and old m.room.member events",
function() {
var sentinel = {
const sentinel = {
userId: userA,
membership: "join",
name: "Alice"
name: "Alice",
};
var oldSentinel = {
const oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice"
name: "Old Alice",
};
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
.andCallFake(function(uid) {
.andCall(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
.andCallFake(function(uid) {
.andCall(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
var newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
const newEv = utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
});
var oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true
const oldEv = utils.mkMembership({
room: roomId, mship: "ban", user: userB, skey: userA, event: true,
});
timeline.addEvent(newEv, false);
expect(newEv.target).toEqual(sentinel);
@@ -248,16 +257,16 @@ describe("EventTimeline", function() {
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for new events", function() {
var events = [
const events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room"
}
})
name: "New room",
},
}),
];
timeline.addEvent(events[0], false);
@@ -272,22 +281,22 @@ describe("EventTimeline", function() {
expect(events[1].forwardLooking).toBe(true);
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
not.toHaveBeenCalled();
toNotHaveBeenCalled();
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for old events", function() {
var events = [
const events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room"
}
})
name: "New room",
},
}),
];
timeline.addEvent(events[0], true);
@@ -302,12 +311,12 @@ describe("EventTimeline", function() {
expect(events[1].forwardLooking).toBe(false);
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
not.toHaveBeenCalled();
toNotHaveBeenCalled();
});
});
describe("removeEvent", function() {
var events = [
const events = [
utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry",
event: true,
@@ -327,7 +336,7 @@ describe("EventTimeline", function() {
timeline.addEvent(events[1], false);
expect(timeline.getEvents().length).toEqual(2);
var ev = timeline.removeEvent(events[0].getId());
let ev = timeline.removeEvent(events[0].getId());
expect(ev).toBe(events[0]);
expect(timeline.getEvents().length).toEqual(1);
@@ -359,7 +368,7 @@ describe("EventTimeline", function() {
function() {
timeline.addEvent(events[0], true);
timeline.removeEvent(events[0].getId());
var initialIndex = timeline.getBaseIndex();
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], false);
timeline.addEvent(events[2], false);
expect(timeline.getBaseIndex()).toEqual(initialIndex);
@@ -367,4 +376,3 @@ describe("EventTimeline", function() {
});
});
});
+18 -15
View File
@@ -1,24 +1,27 @@
"use strict";
var sdk = require("../..");
var Filter = sdk.Filter;
var utils = require("../test-utils");
import 'source-map-support/register';
const sdk = require("../..");
const Filter = sdk.Filter;
const utils = require("../test-utils");
import expect from 'expect';
describe("Filter", function() {
var filterId = "f1lt3ring15g00d4ursoul";
var userId = "@sir_arthur_david:humming.tiger";
var filter;
const filterId = "f1lt3ring15g00d4ursoul";
const userId = "@sir_arthur_david:humming.tiger";
let filter;
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
filter = new Filter(userId);
});
describe("fromJson", function() {
it("create a new Filter from the provided values", function() {
var definition = {
event_fields: ["type", "content"]
const definition = {
event_fields: ["type", "content"],
};
var f = Filter.fromJson(userId, filterId, definition);
const f = Filter.fromJson(userId, filterId, definition);
expect(f.getDefinition()).toEqual(definition);
expect(f.userId).toEqual(userId);
expect(f.filterId).toEqual(filterId);
@@ -31,17 +34,17 @@ describe("Filter", function() {
expect(filter.getDefinition()).toEqual({
room: {
timeline: {
limit: 10
}
}
limit: 10,
},
},
});
});
});
describe("setDefinition/getDefinition", function() {
it("should set and get the filter body", function() {
var definition = {
event_format: "client"
const definition = {
event_format: "client",
};
filter.setDefinition(definition);
expect(filter.getDefinition()).toEqual(definition);
+41 -26
View File
@@ -15,25 +15,37 @@ limitations under the License.
*/
"use strict";
var q = require("q");
var sdk = require("../..");
var utils = require("../test-utils");
import 'source-map-support/register';
const q = require("q");
const sdk = require("../..");
const utils = require("../test-utils");
var InteractiveAuth = sdk.InteractiveAuth;
var MatrixError = sdk.MatrixError;
const InteractiveAuth = sdk.InteractiveAuth;
const MatrixError = sdk.MatrixError;
import expect from 'expect';
// Trivial client object to test interactive auth
// (we do not need TestClient here)
class FakeClient {
generateClientSecret() {
return "testcl1Ent5EcreT";
}
}
describe("InteractiveAuth", function() {
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
});
it("should start an auth stage and complete it", function(done) {
var doRequest = jasmine.createSpy('doRequest');
var startAuthStage = jasmine.createSpy('startAuthStage');
const doRequest = expect.createSpy();
const stateUpdated = expect.createSpy();
var ia = new InteractiveAuth({
const ia = new InteractiveAuth({
matrixClient: new FakeClient(),
doRequest: doRequest,
startAuthStage: startAuthStage,
stateUpdated: stateUpdated,
authData: {
session: "sessionId",
flows: [
@@ -51,7 +63,8 @@ describe("InteractiveAuth", function() {
});
// first we expect a call here
startAuthStage.andCallFake(function(stage) {
stateUpdated.andCall(function(stage) {
console.log('aaaa');
expect(stage).toEqual("logintype");
ia.submitAuthDict({
type: "logintype",
@@ -60,8 +73,9 @@ describe("InteractiveAuth", function() {
});
// .. which should trigger a call here
var requestRes = {"a": "b"};
doRequest.andCallFake(function(authData) {
const requestRes = {"a": "b"};
doRequest.andCall(function(authData) {
console.log('cccc');
expect(authData).toEqual({
session: "sessionId",
type: "logintype",
@@ -73,27 +87,28 @@ describe("InteractiveAuth", function() {
ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest.calls.length).toEqual(1);
expect(startAuthStage.calls.length).toEqual(1);
expect(stateUpdated.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');
const doRequest = expect.createSpy();
const stateUpdated = expect.createSpy();
var ia = new InteractiveAuth({
const ia = new InteractiveAuth({
matrixClient: new FakeClient(),
stateUpdated: stateUpdated,
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) {
doRequest.andCall(function(authData) {
console.log("request1", authData);
expect(authData).toBe(null);
var err = new MatrixError({
expect(authData).toEqual({});
const err = new MatrixError({
session: "sessionId",
flows: [
{ stages: ["logintype"] },
@@ -106,9 +121,9 @@ describe("InteractiveAuth", function() {
throw err;
});
// .. which should be followed by a call to startAuthStage
var requestRes = {"a": "b"};
startAuthStage.andCallFake(function(stage) {
// .. which should be followed by a call to stateUpdated
const requestRes = {"a": "b"};
stateUpdated.andCall(function(stage) {
expect(stage).toEqual("logintype");
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams("logintype")).toEqual({
@@ -116,7 +131,7 @@ describe("InteractiveAuth", function() {
});
// submitAuthDict should trigger another call to doRequest
doRequest.andCallFake(function(authData) {
doRequest.andCall(function(authData) {
console.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
@@ -135,7 +150,7 @@ describe("InteractiveAuth", function() {
ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest.calls.length).toEqual(2);
expect(startAuthStage.calls.length).toEqual(1);
expect(stateUpdated.calls.length).toEqual(1);
}).catch(utils.failTest).done(done);
});
});
+109 -98
View File
@@ -1,44 +1,51 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var MatrixClient = sdk.MatrixClient;
var utils = require("../test-utils");
import 'source-map-support/register';
const q = require("q");
const sdk = require("../..");
const MatrixClient = sdk.MatrixClient;
const utils = require("../test-utils");
import expect from 'expect';
import lolex from 'lolex';
describe("MatrixClient", function() {
var userId = "@alice:bar";
var identityServerUrl = "https://identity.server";
var identityServerDomain = "identity.server";
var client, store, scheduler;
const userId = "@alice:bar";
const identityServerUrl = "https://identity.server";
const identityServerDomain = "identity.server";
let client;
let store;
let scheduler;
let clock;
var KEEP_ALIVE_PATH = "/_matrix/client/versions";
const KEEP_ALIVE_PATH = "/_matrix/client/versions";
var PUSH_RULES_RESPONSE = {
const PUSH_RULES_RESPONSE = {
method: "GET",
path: "/pushrules/",
data: {}
data: {},
};
var FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter";
const FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter";
var FILTER_RESPONSE = {
const FILTER_RESPONSE = {
method: "POST",
path: FILTER_PATH,
data: { filter_id: "f1lt3r" }
data: { filter_id: "f1lt3r" },
};
var SYNC_DATA = {
const SYNC_DATA = {
next_batch: "s_5_3",
presence: { events: [] },
rooms: {}
rooms: {},
};
var SYNC_RESPONSE = {
const SYNC_RESPONSE = {
method: "GET",
path: "/sync",
data: SYNC_DATA
data: SYNC_DATA,
};
var httpLookups = [
let httpLookups = [
// items are objects which look like:
// {
// method: "GET",
@@ -51,14 +58,14 @@ describe("MatrixClient", function() {
// }
// items are popped off when processed and block if no items left.
];
var accept_keepalives;
var pendingLookup = null;
let acceptKeepalives;
let pendingLookup = null;
function httpReq(cb, method, path, qp, data, prefix) {
if (path === KEEP_ALIVE_PATH && accept_keepalives) {
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
return q();
}
var next = httpLookups.shift();
var logLine = (
const next = httpLookups.shift();
const logLine = (
"MatrixClient[UT] RECV " + method + " " + path + " " +
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
);
@@ -73,20 +80,20 @@ describe("MatrixClient", function() {
expect(false).toBe(
true, ">1 pending request. You should probably handle them. " +
"PENDING: " + JSON.stringify(pendingLookup) + " JUST GOT: " +
method + " " + path
method + " " + path,
);
}
pendingLookup = {
promise: q.defer().promise,
method: method,
path: path
path: path,
};
return pendingLookup.promise;
}
if (next.path === path && next.method === method) {
console.log(
"MatrixClient[UT] Matched. Returning " +
(next.error ? "BAD" : "GOOD") + " response"
(next.error ? "BAD" : "GOOD") + " response",
);
if (next.expectBody) {
expect(next.expectBody).toEqual(data);
@@ -107,7 +114,7 @@ describe("MatrixClient", function() {
httpStatus: next.error.httpStatus,
name: next.error.errcode,
message: "Expected testing error",
data: next.error
data: next.error,
});
}
return q(next.data);
@@ -117,17 +124,20 @@ describe("MatrixClient", function() {
}
beforeEach(function() {
utils.beforeEach(this);
jasmine.Clock.useMock();
scheduler = jasmine.createSpyObj("scheduler", [
utils.beforeEach(this); // eslint-disable-line no-invalid-this
clock = lolex.install();
scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction"
]);
store = jasmine.createSpyObj("store", [
"setProcessFunction",
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
store = [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"setSyncToken", "storeEvents", "storeRoom", "storeUser",
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter"
]);
"save", "setSyncToken", "storeEvents", "storeRoom", "storeUser",
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter",
"getSyncAccumulator", "startup", "deleteAllData",
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
store.getSavedSync = expect.createSpy().andReturn(q(null));
store.setSyncData = expect.createSpy().andReturn(q(null));
client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl,
@@ -135,20 +145,20 @@ describe("MatrixClient", function() {
request: function() {}, // NOP
store: store,
scheduler: scheduler,
userId: userId
userId: userId,
});
// FIXME: We shouldn't be yanking _http like this.
client._http = jasmine.createSpyObj("httpApi", [
client._http = [
"authedRequest", "authedRequestWithPrefix", "getContentUri",
"request", "requestWithPrefix", "uploadContent"
]);
client._http.authedRequest.andCallFake(httpReq);
client._http.authedRequestWithPrefix.andCallFake(httpReq);
client._http.requestWithPrefix.andCallFake(httpReq);
client._http.request.andCallFake(httpReq);
"request", "requestWithPrefix", "uploadContent",
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
client._http.authedRequest.andCall(httpReq);
client._http.authedRequestWithPrefix.andCall(httpReq);
client._http.requestWithPrefix.andCall(httpReq);
client._http.request.andCall(httpReq);
// set reasonable working defaults
accept_keepalives = true;
acceptKeepalives = true;
pendingLookup = null;
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
@@ -157,15 +167,16 @@ describe("MatrixClient", function() {
});
afterEach(function() {
clock.uninstall();
// need to re-stub the requests with NOPs because there are no guarantees
// clients from previous tests will be GC'd before the next test. This
// means they may call /events and then fail an expect() which will fail
// a DIFFERENT test (pollution between tests!) - we return unresolved
// promises to stop the client from continuing to run.
client._http.authedRequest.andCallFake(function() {
client._http.authedRequest.andCall(function() {
return q.defer().promise;
});
client._http.authedRequestWithPrefix.andCallFake(function() {
client._http.authedRequestWithPrefix.andCall(function() {
return q.defer().promise;
});
});
@@ -174,9 +185,9 @@ describe("MatrixClient", function() {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
var filterId = "ehfewf";
const filterId = "ehfewf";
store.getFilterIdByName.andReturn(filterId);
var filter = new sdk.Filter(0, filterId);
const filter = new sdk.Filter(0, filterId);
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
store.getFilter.andReturn(filter);
client.startClient();
@@ -191,9 +202,8 @@ describe("MatrixClient", function() {
});
describe("getSyncState", function() {
it("should return null if the client isn't started", function() {
expect(client.getSyncState()).toBeNull();
expect(client.getSyncState()).toBe(null);
});
it("should return the same sync state as emitted sync events", function(done) {
@@ -219,7 +229,7 @@ describe("MatrixClient", function() {
// and they all need to be stored!
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
}
var invalidFilterId = 'invalidF1lt3r';
const invalidFilterId = 'invalidF1lt3r';
httpLookups = [];
httpLookups.push({
method: "GET",
@@ -229,15 +239,15 @@ describe("MatrixClient", function() {
name: "M_UNKNOWN",
message: "No row found",
data: { errcode: "M_UNKNOWN", error: "No row found" },
httpStatus: 404
}
httpStatus: 404,
},
});
httpLookups.push(FILTER_RESPONSE);
store.getFilterIdByName.andReturn(invalidFilterId);
var filterName = getFilterName(client.credentials.userId);
const filterName = getFilterName(client.credentials.userId);
client.store.setFilterIdByName(filterName, invalidFilterId);
var filter = new sdk.Filter(client.credentials.userId);
const filter = new sdk.Filter(client.credentials.userId);
client.getOrCreateFilter(filterName, filter).then(function(filterId) {
expect(filterId).toEqual(FILTER_RESPONSE.data.filter_id);
@@ -256,7 +266,7 @@ describe("MatrixClient", function() {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
@@ -265,7 +275,7 @@ describe("MatrixClient", function() {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(2);
expect(client.retryImmediately()).toBe(true);
jasmine.Clock.tick(1);
clock.tick(1);
} else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
@@ -279,21 +289,21 @@ describe("MatrixClient", function() {
it("should work on /sync", function(done) {
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA
method: "GET", path: "/sync", data: SYNC_DATA,
});
client.on("sync", function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(1);
expect(client.retryImmediately()).toBe(
true, "retryImmediately returned false"
true, "retryImmediately returned false",
);
jasmine.Clock.tick(1);
clock.tick(1);
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
jasmine.Clock.tick(10000);
clock.tick(10000);
} else if (state === "SYNCING" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
@@ -305,7 +315,7 @@ describe("MatrixClient", function() {
it("should work on /pushrules", function(done) {
httpLookups = [];
httpLookups.push({
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" }
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
@@ -315,7 +325,7 @@ describe("MatrixClient", function() {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(3);
expect(client.retryImmediately()).toBe(true);
jasmine.Clock.tick(1);
clock.tick(1);
} else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
@@ -329,12 +339,11 @@ describe("MatrixClient", function() {
});
describe("emitted sync events", function() {
function syncChecker(expectedStates, done) {
return function syncListener(state, old) {
var expected = expectedStates.shift();
const expected = expectedStates.shift();
console.log(
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected,
);
if (!expected) {
done();
@@ -347,23 +356,23 @@ describe("MatrixClient", function() {
done();
}
// standard retry time is 5 to 10 seconds
jasmine.Clock.tick(10000);
clock.tick(10000);
};
}
it("should transition null -> PREPARED after the first /sync", function(done) {
var expectedStates = [];
const expectedStates = [];
expectedStates.push(["PREPARED", null]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition null -> ERROR after a failed /filter", function(done) {
var expectedStates = [];
const expectedStates = [];
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
});
expectedStates.push(["ERROR", null]);
client.on("sync", syncChecker(expectedStates, done));
@@ -372,22 +381,23 @@ describe("MatrixClient", function() {
it("should transition ERROR -> PREPARED after /sync if prev failed",
function(done) {
var expectedStates = [];
accept_keepalives = false;
const expectedStates = [];
acceptKeepalives = false;
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, data: {}
method: "GET", path: KEEP_ALIVE_PATH, data: {},
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA
method: "GET", path: "/sync", data: SYNC_DATA,
});
expectedStates.push(["RECONNECTING", null]);
@@ -398,7 +408,7 @@ describe("MatrixClient", function() {
});
it("should transition PREPARED -> SYNCING after /sync", function(done) {
var expectedStates = [];
const expectedStates = [];
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
client.on("sync", syncChecker(expectedStates, done));
@@ -406,13 +416,14 @@ describe("MatrixClient", function() {
});
it("should transition SYNCING -> ERROR after a failed /sync", function(done) {
accept_keepalives = false;
var expectedStates = [];
acceptKeepalives = false;
const expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
expectedStates.push(["PREPARED", null]);
@@ -425,9 +436,9 @@ describe("MatrixClient", function() {
xit("should transition ERROR -> SYNCING after /sync if prev failed",
function(done) {
var expectedStates = [];
const expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
httpLookups.push(SYNC_RESPONSE);
@@ -440,7 +451,7 @@ describe("MatrixClient", function() {
it("should transition SYNCING -> SYNCING on subsequent /sync successes",
function(done) {
var expectedStates = [];
const expectedStates = [];
httpLookups.push(SYNC_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
@@ -452,16 +463,18 @@ describe("MatrixClient", function() {
});
it("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
accept_keepalives = false;
var expectedStates = [];
acceptKeepalives = false;
const expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, error: { errcode: "KEEPALIVE_FAIL" }
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
expectedStates.push(["PREPARED", null]);
@@ -475,7 +488,7 @@ describe("MatrixClient", function() {
});
describe("inviteByEmail", function() {
var roomId = "!foo:bar";
const roomId = "!foo:bar";
it("should send an invite HTTP POST", function() {
httpLookups = [{
@@ -485,17 +498,15 @@ describe("MatrixClient", function() {
expectBody: {
id_server: identityServerDomain,
medium: "email",
address: "alice@gmail.com"
}
address: "alice@gmail.com",
},
}];
client.inviteByEmail(roomId, "alice@gmail.com");
expect(httpLookups.length).toEqual(0);
});
});
describe("guest rooms", function() {
it("should only do /sync calls (without filter/pushrules)", function(done) {
httpLookups = []; // no /pushrules or /filter
httpLookups.push({
@@ -504,7 +515,7 @@ describe("MatrixClient", function() {
data: SYNC_DATA,
thenCall: function() {
done();
}
},
});
client.setGuest(true);
client.startClient();
+78 -76
View File
@@ -1,33 +1,35 @@
"use strict";
var PushProcessor = require("../../lib/pushprocessor");
var MatrixEvent = MatrixEvent;
var utils = require("../test-utils");
import 'source-map-support/register';
const PushProcessor = require("../../lib/pushprocessor");
const utils = require("../test-utils");
import expect from 'expect';
describe('NotificationService', function() {
var testUserId = "@ali:matrix.org";
var testDisplayName = "Alice M";
var testRoomId = "!fl1bb13:localhost";
const testUserId = "@ali:matrix.org";
const testDisplayName = "Alice M";
const testRoomId = "!fl1bb13:localhost";
var testEvent;
let testEvent;
var pushProcessor;
let pushProcessor;
// These would be better if individual rules were configured in the tests themselves.
var matrixClient = {
const matrixClient = {
getRoom: function() {
return {
currentState: {
getMember: function() {
return {
name: testDisplayName
name: testDisplayName,
};
},
members: {}
}
members: {},
},
};
},
credentials: {
userId: testUserId
userId: testUserId,
},
pushRules: {
"device": {},
@@ -38,91 +40,91 @@ describe('NotificationService', function() {
"notify",
{
"set_tweak": "sound",
"value": "default"
"value": "default",
},
{
"set_tweak": "highlight"
}
"set_tweak": "highlight",
},
],
"enabled": true,
"pattern": "ali",
"rule_id": ".m.rule.contains_user_name"
"rule_id": ".m.rule.contains_user_name",
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
"value": "default",
},
{
"set_tweak": "highlight"
}
"set_tweak": "highlight",
},
],
"enabled": true,
"pattern": "coffee",
"rule_id": "coffee"
"rule_id": "coffee",
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
"value": "default",
},
{
"set_tweak": "highlight"
}
"set_tweak": "highlight",
},
],
"enabled": true,
"pattern": "foo*bar",
"rule_id": "foobar"
"rule_id": "foobar",
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
"value": "default",
},
{
"set_tweak": "highlight"
}
"set_tweak": "highlight",
},
],
"enabled": true,
"pattern": "p[io]ng",
"rule_id": "pingpong"
"rule_id": "pingpong",
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
"value": "default",
},
{
"set_tweak": "highlight"
}
"set_tweak": "highlight",
},
],
"enabled": true,
"pattern": "I ate [0-9] pies",
"rule_id": "pies"
"rule_id": "pies",
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
"value": "default",
},
{
"set_tweak": "highlight"
}
"set_tweak": "highlight",
},
],
"enabled": true,
"pattern": "b[!ai]ke",
"rule_id": "bakebike"
}
"rule_id": "bakebike",
},
],
"override": [
{
@@ -130,70 +132,70 @@ describe('NotificationService', function() {
"notify",
{
"set_tweak": "sound",
"value": "default"
"value": "default",
},
{
"set_tweak": "highlight"
}
"set_tweak": "highlight",
},
],
"conditions": [
{
"kind": "contains_display_name"
}
"kind": "contains_display_name",
},
],
"enabled": true,
"rule_id": ".m.rule.contains_display_name"
"rule_id": ".m.rule.contains_display_name",
},
{
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
}
"value": "default",
},
],
"conditions": [
{
"is": "2",
"kind": "room_member_count"
}
"kind": "room_member_count",
},
],
"enabled": true,
"rule_id": ".m.rule.room_one_to_one"
}
"rule_id": ".m.rule.room_one_to_one",
},
],
"room": [],
"sender": [],
"underride": [
{
"actions": [
"dont-notify"
"dont-notify",
],
"conditions": [
{
"key": "content.msgtype",
"kind": "event_match",
"pattern": "m.notice"
}
"pattern": "m.notice",
},
],
"enabled": true,
"rule_id": ".m.rule.suppress_notices"
"rule_id": ".m.rule.suppress_notices",
},
{
"actions": [
"notify",
{
"set_tweak": "highlight",
"value": false
}
"value": false,
},
],
"conditions": [],
"enabled": true,
"rule_id": ".m.rule.fallback"
}
]
}
}
"rule_id": ".m.rule.fallback",
},
],
},
},
};
beforeEach(function() {
@@ -204,8 +206,8 @@ describe('NotificationService', function() {
event: true,
content: {
body: "",
msgtype: "m.text"
}
msgtype: "m.text",
},
});
pushProcessor = new PushProcessor(matrixClient);
});
@@ -214,25 +216,25 @@ describe('NotificationService', function() {
it('should bing on a user ID.', function() {
testEvent.event.content.body = "Hello @ali:matrix.org, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a partial user ID with an @.', function() {
testEvent.event.content.body = "Hello @ali, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a partial user ID without @.', function() {
testEvent.event.content.body = "Hello ali, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a case-insensitive user ID.', function() {
testEvent.event.content.body = "Hello @AlI:matrix.org, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
@@ -240,13 +242,13 @@ describe('NotificationService', function() {
it('should bing on a display name.', function() {
testEvent.event.content.body = "Hello Alice M, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on a case-insensitive display name.', function() {
testEvent.event.content.body = "Hello ALICE M, how are you?";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
@@ -254,25 +256,25 @@ describe('NotificationService', function() {
it('should bing on a bing word.', function() {
testEvent.event.content.body = "I really like coffee";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on case-insensitive bing words.', function() {
testEvent.event.content.body = "Coffee is great";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on wildcard (.*) bing words.', function() {
testEvent.event.content.body = "It was foomahbar I think.";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on character group ([abc]) bing words.', function() {
testEvent.event.content.body = "Ping!";
var actions = pushProcessor.actionsForEvent(testEvent);
let actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
testEvent.event.content.body = "Pong!";
actions = pushProcessor.actionsForEvent(testEvent);
@@ -281,13 +283,13 @@ describe('NotificationService', function() {
it('should bing on character range ([a-z]) bing words.', function() {
testEvent.event.content.body = "I ate 6 pies";
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on character negation ([!a]) bing words.', function() {
testEvent.event.content.body = "boke";
var actions = pushProcessor.actionsForEvent(testEvent);
let actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
testEvent.event.content.body = "bake";
actions = pushProcessor.actionsForEvent(testEvent);
@@ -298,7 +300,7 @@ describe('NotificationService', function() {
it('should gracefully handle bad input.', function() {
testEvent.event.content.body = { "foo": "bar" };
var actions = pushProcessor.actionsForEvent(testEvent);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(false);
});
});
+64 -59
View File
@@ -1,97 +1,100 @@
"use strict";
var callbacks = require("../../lib/realtime-callbacks");
var test_utils = require("../test-utils.js");
import 'source-map-support/register';
const callbacks = require("../../lib/realtime-callbacks");
const testUtils = require("../test-utils.js");
import expect from 'expect';
import lolex from 'lolex';
describe("realtime-callbacks", function() {
var clock = jasmine.Clock;
var fakeDate;
let clock;
function tick(millis) {
// make sure we tick the fakedate first, otherwise nothing will happen!
fakeDate += millis;
clock.tick(millis);
}
beforeEach(function() {
test_utils.beforeEach(this);
clock.useMock();
fakeDate = Date.now();
callbacks.setNow(function() { return fakeDate; });
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
clock = lolex.install();
const fakeDate = clock.Date;
callbacks.setNow(fakeDate.now.bind(fakeDate));
});
afterEach(function() {
callbacks.setNow();
clock.uninstall();
});
describe("setTimeout", function() {
it("should call the callback after the timeout", function() {
var callback = jasmine.createSpy();
const callback = expect.createSpy();
callbacks.setTimeout(callback, 100);
expect(callback).not.toHaveBeenCalled();
expect(callback).toNotHaveBeenCalled();
tick(100);
expect(callback).toHaveBeenCalled();
});
it("should default to a zero timeout", function() {
var callback = jasmine.createSpy();
const callback = expect.createSpy();
callbacks.setTimeout(callback);
expect(callback).not.toHaveBeenCalled();
expect(callback).toNotHaveBeenCalled();
tick(0);
expect(callback).toHaveBeenCalled();
});
it("should pass any parameters to the callback", function() {
var callback = jasmine.createSpy();
const callback = expect.createSpy();
callbacks.setTimeout(callback, 0, "a", "b", "c");
tick(0);
expect(callback).toHaveBeenCalledWith("a", "b", "c");
});
it("should set 'this' to the global object", function() {
var callback = jasmine.createSpy();
callback.andCallFake(function() {
expect(this).toBe(global);
expect(this.console).toBeDefined();
});
let passed = false;
const callback = function() {
expect(this).toBe(global); // eslint-disable-line no-invalid-this
expect(this.console).toBeTruthy(); // eslint-disable-line no-invalid-this
passed = true;
};
callbacks.setTimeout(callback);
tick(0);
expect(callback).toHaveBeenCalled();
expect(passed).toBe(true);
});
it("should handle timeouts of several seconds", function() {
var callback = jasmine.createSpy();
const callback = expect.createSpy();
callbacks.setTimeout(callback, 2000);
expect(callback).not.toHaveBeenCalled();
for (var i = 0; i < 4; i++) {
expect(callback).toNotHaveBeenCalled();
for (let i = 0; i < 4; i++) {
tick(500);
}
expect(callback).toHaveBeenCalled();
});
it("should call multiple callbacks in the right order", function() {
var callback1 = jasmine.createSpy("callback1");
var callback2 = jasmine.createSpy("callback2");
var callback3 = jasmine.createSpy("callback3");
const callback1 = expect.createSpy();
const callback2 = expect.createSpy();
const callback3 = expect.createSpy();
callbacks.setTimeout(callback2, 200);
callbacks.setTimeout(callback1, 100);
callbacks.setTimeout(callback3, 300);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
expect(callback1).toNotHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
expect(callback3).toNotHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
expect(callback3).toNotHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
expect(callback3).toNotHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
@@ -99,81 +102,83 @@ describe("realtime-callbacks", function() {
});
it("should treat -ve timeouts the same as a zero timeout", function() {
var callback1 = jasmine.createSpy("callback1");
var callback2 = jasmine.createSpy("callback2");
const callback1 = expect.createSpy();
const callback2 = expect.createSpy();
// check that cb1 is called before cb2
callback1.andCallFake(function() {
expect(callback2).not.toHaveBeenCalled();
callback1.andCall(function() {
expect(callback2).toNotHaveBeenCalled();
});
callbacks.setTimeout(callback1);
callbacks.setTimeout(callback2, -100);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback1).toNotHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
it("should not get confused by chained calls", function() {
var callback2 = jasmine.createSpy("callback2");
var callback1 = jasmine.createSpy("callback1");
callback1.andCallFake(function() {
const callback2 = expect.createSpy();
const callback1 = expect.createSpy();
callback1.andCall(function() {
callbacks.setTimeout(callback2, 0);
expect(callback2).not.toHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
});
callbacks.setTimeout(callback1);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback1).toNotHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
// the fake timer won't actually run callbacks registered during
// one tick until the next tick.
tick(1);
expect(callback2).toHaveBeenCalled();
});
it("should be immune to exceptions", function() {
var callback1 = jasmine.createSpy("callback1");
callback1.andCallFake(function() {
const callback1 = expect.createSpy();
callback1.andCall(function() {
throw new Error("prepare to die");
});
var callback2 = jasmine.createSpy("callback2");
const callback2 = expect.createSpy();
callbacks.setTimeout(callback1, 0);
callbacks.setTimeout(callback2, 0);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback1).toNotHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
});
describe("cancelTimeout", function() {
it("should cancel a pending timeout", function() {
var callback = jasmine.createSpy();
var k = callbacks.setTimeout(callback);
const callback = expect.createSpy();
const k = callbacks.setTimeout(callback);
callbacks.clearTimeout(k);
tick(0);
expect(callback).not.toHaveBeenCalled();
expect(callback).toNotHaveBeenCalled();
});
it("should not affect sooner timeouts", function() {
var callback1 = jasmine.createSpy("callback1");
var callback2 = jasmine.createSpy("callback2");
const callback1 = expect.createSpy();
const callback2 = expect.createSpy();
callbacks.setTimeout(callback1, 100);
var k = callbacks.setTimeout(callback2, 200);
const k = callbacks.setTimeout(callback2, 200);
callbacks.clearTimeout(k);
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
tick(150);
expect(callback2).not.toHaveBeenCalled();
expect(callback2).toNotHaveBeenCalled();
});
});
});
+60 -58
View File
@@ -1,22 +1,25 @@
"use strict";
var sdk = require("../..");
var RoomMember = sdk.RoomMember;
var utils = require("../test-utils");
import 'source-map-support/register';
const sdk = require("../..");
const RoomMember = sdk.RoomMember;
const utils = require("../test-utils");
import expect from 'expect';
describe("RoomMember", function() {
var roomId = "!foo:bar";
var userA = "@alice:bar";
var userB = "@bertha:bar";
var userC = "@clarissa:bar";
var member;
const roomId = "!foo:bar";
const userA = "@alice:bar";
const userB = "@bertha:bar";
const userC = "@clarissa:bar";
let member;
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
member = new RoomMember(roomId, userA);
});
describe("getAvatarUrl", function() {
var hsUrl = "https://my.home.server";
const hsUrl = "https://my.home.server";
it("should return the URL from m.room.member preferentially", function() {
member.events.member = utils.mkEvent({
@@ -27,31 +30,31 @@ describe("RoomMember", function() {
user: userA,
content: {
membership: "join",
avatar_url: "mxc://flibble/wibble"
}
avatar_url: "mxc://flibble/wibble",
},
});
var url = member.getAvatarUrl(hsUrl);
const url = member.getAvatarUrl(hsUrl);
// we don't care about how the mxc->http conversion is done, other
// than it contains the mxc body.
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
expect(url.indexOf("flibble/wibble")).toNotEqual(-1);
});
it("should return an identicon HTTP URL if allowDefault was set and there " +
"was no m.room.member event", function() {
var url = member.getAvatarUrl(hsUrl, 64, 64, "crop", true);
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", true);
expect(url.indexOf("http")).toEqual(0); // don't care about form
});
it("should return nothing if there is no m.room.member and allowDefault=false",
function() {
var url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
expect(url).toEqual(null);
});
});
describe("setPowerLevelEvent", function() {
it("should set 'powerLevel' and 'powerLevelNorm'.", function() {
var event = utils.mkEvent({
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
@@ -59,16 +62,16 @@ describe("RoomMember", function() {
users_default: 20,
users: {
"@bertha:bar": 200,
"@invalid:user": 10 // shouldn't barf on this.
}
"@invalid:user": 10, // shouldn't barf on this.
},
},
event: true
event: true,
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(20);
expect(member.powerLevelNorm).toEqual(10);
var memberB = new RoomMember(roomId, userB);
const memberB = new RoomMember(roomId, userB);
memberB.setPowerLevelEvent(event);
expect(memberB.powerLevel).toEqual(200);
expect(memberB.powerLevelNorm).toEqual(100);
@@ -76,7 +79,7 @@ describe("RoomMember", function() {
it("should emit 'RoomMember.powerLevel' if the power level changes.",
function() {
var event = utils.mkEvent({
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
@@ -84,12 +87,12 @@ describe("RoomMember", function() {
users_default: 20,
users: {
"@bertha:bar": 200,
"@invalid:user": 10 // shouldn't barf on this.
}
"@invalid:user": 10, // shouldn't barf on this.
},
},
event: true
event: true,
});
var emitCount = 0;
let emitCount = 0;
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
emitCount += 1;
@@ -105,7 +108,7 @@ describe("RoomMember", function() {
it("should honour power levels of zero.",
function() {
var event = utils.mkEvent({
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
@@ -113,11 +116,11 @@ describe("RoomMember", function() {
users_default: 20,
users: {
"@alice:bar": 0,
}
},
},
event: true
event: true,
});
var emitCount = 0;
let emitCount = 0;
// set the power level to something other than zero or we
// won't get an event
@@ -138,21 +141,21 @@ describe("RoomMember", function() {
describe("setTypingEvent", function() {
it("should set 'typing'", function() {
member.typing = false;
var memberB = new RoomMember(roomId, userB);
const memberB = new RoomMember(roomId, userB);
memberB.typing = true;
var memberC = new RoomMember(roomId, userC);
const memberC = new RoomMember(roomId, userC);
memberC.typing = true;
var event = utils.mkEvent({
const event = utils.mkEvent({
type: "m.typing",
user: userA,
room: roomId,
content: {
user_ids: [
userA, userC
]
userA, userC,
],
},
event: true
event: true,
});
member.setTypingEvent(event);
memberB.setTypingEvent(event);
@@ -165,17 +168,17 @@ describe("RoomMember", function() {
it("should emit 'RoomMember.typing' if the typing state changes",
function() {
var event = utils.mkEvent({
const event = utils.mkEvent({
type: "m.typing",
room: roomId,
content: {
user_ids: [
userA, userC
]
userA, userC,
],
},
event: true
event: true,
});
var emitCount = 0;
let emitCount = 0;
member.on("RoomMember.typing", function(ev, mem) {
expect(mem).toEqual(member);
expect(ev).toEqual(event);
@@ -190,20 +193,20 @@ describe("RoomMember", function() {
});
describe("setMembershipEvent", function() {
var joinEvent = utils.mkMembership({
const joinEvent = utils.mkMembership({
event: true,
mship: "join",
user: userA,
room: roomId,
name: "Alice"
name: "Alice",
});
var inviteEvent = utils.mkMembership({
const inviteEvent = utils.mkMembership({
event: true,
mship: "invite",
user: userB,
skey: userA,
room: roomId
room: roomId,
});
it("should set 'membership' and assign the event to 'events.member'.",
@@ -218,36 +221,38 @@ describe("RoomMember", function() {
it("should set 'name' based on user_id, displayname and room state",
function() {
var roomState = {
const roomState = {
getStateEvents: function(type) {
if (type !== "m.room.member") { return []; }
if (type !== "m.room.member") {
return [];
}
return [
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userB
user: userB,
}),
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userC, name: "Alice"
user: userC, name: "Alice",
}),
joinEvent
joinEvent,
];
},
getUserIdsWithDisplayName: function(displayName) {
return [userA, userC];
}
},
};
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent);
expect(member.name).toEqual("Alice"); // prefer displayname
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).not.toEqual("Alice"); // it should disambig.
expect(member.name).toNotEqual("Alice"); // it should disambig.
// user_id should be there somewhere
expect(member.name.indexOf(userA)).not.toEqual(-1);
expect(member.name.indexOf(userA)).toNotEqual(-1);
});
it("should emit 'RoomMember.membership' if the membership changes", function() {
var emitCount = 0;
let emitCount = 0;
member.on("RoomMember.membership", function(ev, mem) {
emitCount += 1;
expect(mem).toEqual(member);
@@ -260,7 +265,7 @@ describe("RoomMember", function() {
});
it("should emit 'RoomMember.name' if the name changes", function() {
var emitCount = 0;
let emitCount = 0;
member.on("RoomMember.name", function(ev, mem) {
emitCount += 1;
expect(mem).toEqual(member);
@@ -271,8 +276,5 @@ describe("RoomMember", function() {
member.setMembershipEvent(joinEvent); // no-op
expect(emitCount).toEqual(1);
});
});
});
+109 -106
View File
@@ -1,35 +1,38 @@
"use strict";
var sdk = require("../..");
var RoomState = sdk.RoomState;
var RoomMember = sdk.RoomMember;
var utils = require("../test-utils");
import 'source-map-support/register';
const sdk = require("../..");
const RoomState = sdk.RoomState;
const RoomMember = sdk.RoomMember;
const utils = require("../test-utils");
import expect from 'expect';
describe("RoomState", function() {
var roomId = "!foo:bar";
var userA = "@alice:bar";
var userB = "@bob:bar";
var state;
const roomId = "!foo:bar";
const userA = "@alice:bar";
const userB = "@bob:bar";
let state;
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
state = new RoomState(roomId);
state.setStateEvents([
utils.mkMembership({ // userA joined
event: true, mship: "join", user: userA, room: roomId
event: true, mship: "join", user: userA, room: roomId,
}),
utils.mkMembership({ // userB joined
event: true, mship: "join", user: userB, room: roomId
event: true, mship: "join", user: userB, room: roomId,
}),
utils.mkEvent({ // Room name is "Room name goes here"
type: "m.room.name", user: userA, room: roomId, event: true, content: {
name: "Room name goes here"
}
name: "Room name goes here",
},
}),
utils.mkEvent({ // Room creation
type: "m.room.create", user: userA, room: roomId, event: true, content: {
creator: userA
}
})
creator: userA,
},
}),
]);
});
@@ -40,11 +43,11 @@ describe("RoomState", function() {
});
it("should return a member for each m.room.member event", function() {
var members = state.getMembers();
const members = state.getMembers();
expect(members.length).toEqual(2);
// ordering unimportant
expect([userA, userB].indexOf(members[0].userId)).not.toEqual(-1);
expect([userA, userB].indexOf(members[1].userId)).not.toEqual(-1);
expect([userA, userB].indexOf(members[0].userId)).toNotEqual(-1);
expect([userA, userB].indexOf(members[1].userId)).toNotEqual(-1);
});
});
@@ -54,19 +57,19 @@ describe("RoomState", function() {
});
it("should return a member if they exist", function() {
expect(state.getMember(userB)).toBeDefined();
expect(state.getMember(userB)).toBeTruthy();
});
it("should return a member which changes as state changes", function() {
var member = state.getMember(userB);
const member = state.getMember(userB);
expect(member.membership).toEqual("join");
expect(member.name).toEqual(userB);
state.setStateEvents([
utils.mkMembership({
room: roomId, user: userB, mship: "leave", event: true,
name: "BobGone"
})
name: "BobGone",
}),
]);
expect(member.membership).toEqual("leave");
@@ -81,14 +84,14 @@ describe("RoomState", function() {
it("should return a member which doesn't change when the state is updated",
function() {
var preLeaveUser = state.getSentinelMember(userA);
const preLeaveUser = state.getSentinelMember(userA);
state.setStateEvents([
utils.mkMembership({
room: roomId, user: userA, mship: "leave", event: true,
name: "AliceIsGone"
})
name: "AliceIsGone",
}),
]);
var postLeaveUser = state.getSentinelMember(userA);
const postLeaveUser = state.getSentinelMember(userA);
expect(preLeaveUser.membership).toEqual("join");
expect(preLeaveUser.name).toEqual(userA);
@@ -111,33 +114,33 @@ describe("RoomState", function() {
it("should return a list of matching events if no state_key was specified",
function() {
var events = state.getStateEvents("m.room.member");
const events = state.getStateEvents("m.room.member");
expect(events.length).toEqual(2);
// ordering unimportant
expect([userA, userB].indexOf(events[0].getStateKey())).not.toEqual(-1);
expect([userA, userB].indexOf(events[1].getStateKey())).not.toEqual(-1);
expect([userA, userB].indexOf(events[0].getStateKey())).toNotEqual(-1);
expect([userA, userB].indexOf(events[1].getStateKey())).toNotEqual(-1);
});
it("should return a single MatrixEvent if a state_key was specified",
function() {
var event = state.getStateEvents("m.room.member", userA);
const event = state.getStateEvents("m.room.member", userA);
expect(event.getContent()).toEqual({
membership: "join"
membership: "join",
});
});
});
describe("setStateEvents", function() {
it("should emit 'RoomState.members' for each m.room.member event", function() {
var memberEvents = [
const memberEvents = [
utils.mkMembership({
user: "@cleo:bar", mship: "invite", room: roomId, event: true
user: "@cleo:bar", mship: "invite", room: roomId, event: true,
}),
utils.mkMembership({
user: "@daisy:bar", mship: "join", room: roomId, event: true
})
user: "@daisy:bar", mship: "join", room: roomId, event: true,
}),
];
var emitCount = 0;
let emitCount = 0;
state.on("RoomState.members", function(ev, st, mem) {
expect(ev).toEqual(memberEvents[emitCount]);
expect(st).toEqual(state);
@@ -149,15 +152,15 @@ describe("RoomState", function() {
});
it("should emit 'RoomState.newMember' for each new member added", function() {
var memberEvents = [
const memberEvents = [
utils.mkMembership({
user: "@cleo:bar", mship: "invite", room: roomId, event: true
user: "@cleo:bar", mship: "invite", room: roomId, event: true,
}),
utils.mkMembership({
user: "@daisy:bar", mship: "join", room: roomId, event: true
})
user: "@daisy:bar", mship: "join", room: roomId, event: true,
}),
];
var emitCount = 0;
let emitCount = 0;
state.on("RoomState.newMember", function(ev, st, mem) {
expect(mem.userId).toEqual(memberEvents[emitCount].getSender());
expect(mem.membership).toBeFalsy(); // not defined yet
@@ -168,21 +171,21 @@ describe("RoomState", function() {
});
it("should emit 'RoomState.events' for each state event", function() {
var events = [
const events = [
utils.mkMembership({
user: "@cleo:bar", mship: "invite", room: roomId, event: true
user: "@cleo:bar", mship: "invite", room: roomId, event: true,
}),
utils.mkEvent({
user: userB, room: roomId, type: "m.room.topic", event: true,
content: {
topic: "boo!"
}
topic: "boo!",
},
}),
utils.mkMessage({ // Not a state event
user: userA, room: roomId, event: true
})
user: userA, room: roomId, event: true,
}),
];
var emitCount = 0;
let emitCount = 0;
state.on("RoomState.events", function(ev, st) {
expect(ev).toEqual(events[emitCount]);
expect(st).toEqual(state);
@@ -198,39 +201,39 @@ describe("RoomState", function() {
state.members[userA] = utils.mock(RoomMember);
state.members[userB] = utils.mock(RoomMember);
var powerLevelEvent = utils.mkEvent({
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 50,
events_default: 25
}
});
state.setStateEvents([powerLevelEvent]);
expect(state.members[userA].setPowerLevelEvent).toHaveBeenCalledWith(
powerLevelEvent
);
expect(state.members[userB].setPowerLevelEvent).toHaveBeenCalledWith(
powerLevelEvent
);
});
it("should call setPowerLevelEvent on a new RoomMember if power levels exist",
function() {
var userC = "@cleo:bar";
var memberEvent = utils.mkMembership({
mship: "join", user: userC, room: roomId, event: true
});
var powerLevelEvent = utils.mkEvent({
const powerLevelEvent = utils.mkEvent({
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 50,
events_default: 25,
users: {}
}
},
});
state.setStateEvents([powerLevelEvent]);
expect(state.members[userA].setPowerLevelEvent).toHaveBeenCalledWith(
powerLevelEvent,
);
expect(state.members[userB].setPowerLevelEvent).toHaveBeenCalledWith(
powerLevelEvent,
);
});
it("should call setPowerLevelEvent on a new RoomMember if power levels exist",
function() {
const userC = "@cleo:bar";
const memberEvent = utils.mkMembership({
mship: "join", user: userC, room: roomId, event: true,
});
const powerLevelEvent = utils.mkEvent({
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 50,
events_default: 25,
users: {},
},
});
state.setStateEvents([powerLevelEvent]);
@@ -238,7 +241,7 @@ describe("RoomState", function() {
// TODO: We do this because we don't DI the RoomMember constructor
// so we can't inject a mock :/ so we have to infer.
expect(state.members[userC]).toBeDefined();
expect(state.members[userC]).toBeTruthy();
expect(state.members[userC].powerLevel).toEqual(10);
});
@@ -247,24 +250,24 @@ describe("RoomState", function() {
state.members[userA] = utils.mock(RoomMember);
state.members[userB] = utils.mock(RoomMember);
var memberEvent = utils.mkMembership({
user: userB, mship: "leave", room: roomId, event: true
const memberEvent = utils.mkMembership({
user: userB, mship: "leave", room: roomId, event: true,
});
state.setStateEvents([memberEvent]);
expect(state.members[userA].setMembershipEvent).not.toHaveBeenCalled();
expect(state.members[userA].setMembershipEvent).toNotHaveBeenCalled();
expect(state.members[userB].setMembershipEvent).toHaveBeenCalledWith(
memberEvent, state
memberEvent, state,
);
});
});
describe("setTypingEvent", function() {
it("should call setTypingEvent on each RoomMember", function() {
var typingEvent = utils.mkEvent({
const typingEvent = utils.mkEvent({
type: "m.typing", room: roomId, event: true, content: {
user_ids: [userA]
}
user_ids: [userA],
},
});
// mock up the room members
state.members[userA] = utils.mock(RoomMember);
@@ -272,10 +275,10 @@ describe("RoomState", function() {
state.setTypingEvent(typingEvent);
expect(state.members[userA].setTypingEvent).toHaveBeenCalledWith(
typingEvent
typingEvent,
);
expect(state.members[userB].setTypingEvent).toHaveBeenCalledWith(
typingEvent
typingEvent,
);
});
});
@@ -284,7 +287,7 @@ describe("RoomState", function() {
it("should say non-joined members may not send state",
function() {
expect(state.maySendStateEvent(
'm.room.name', "@nobody:nowhere"
'm.room.name', "@nobody:nowhere",
)).toEqual(false);
});
@@ -296,15 +299,15 @@ describe("RoomState", function() {
it("should say members with power >=50 may send state with power level event " +
"but no state default",
function() {
var powerLevelEvent = {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
// state_default: 50, "intentionally left blank"
events_default: 25,
users: {
}
}
},
},
};
powerLevelEvent.content.users[userA] = 50;
@@ -316,15 +319,15 @@ describe("RoomState", function() {
it("should obey state_default",
function() {
var powerLevelEvent = {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 30,
events_default: 25,
users: {
}
}
},
},
};
powerLevelEvent.content.users[userA] = 30;
powerLevelEvent.content.users[userB] = 29;
@@ -337,18 +340,18 @@ describe("RoomState", function() {
it("should honour explicit event power levels in the power_levels event",
function() {
var powerLevelEvent = {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
events: {
"m.room.other_thing": 76
"m.room.other_thing": 76,
},
users_default: 10,
state_default: 50,
events_default: 25,
users: {
}
}
},
},
};
powerLevelEvent.content.users[userA] = 80;
powerLevelEvent.content.users[userB] = 50;
@@ -367,7 +370,7 @@ describe("RoomState", function() {
it("should say non-joined members may not send events",
function() {
expect(state.maySendEvent(
'm.room.message', "@nobody:nowhere"
'm.room.message', "@nobody:nowhere",
)).toEqual(false);
expect(state.maySendMessage("@nobody:nowhere")).toEqual(false);
});
@@ -380,15 +383,15 @@ describe("RoomState", function() {
it("should obey events_default",
function() {
var powerLevelEvent = {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 30,
events_default: 25,
users: {
}
}
},
},
};
powerLevelEvent.content.users[userA] = 26;
powerLevelEvent.content.users[userB] = 24;
@@ -404,18 +407,18 @@ describe("RoomState", function() {
it("should honour explicit event power levels in the power_levels event",
function() {
var powerLevelEvent = {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
events: {
"m.room.other_thing": 33
"m.room.other_thing": 33,
},
users_default: 10,
state_default: 50,
events_default: 25,
users: {
}
}
},
},
};
powerLevelEvent.content.users[userA] = 40;
powerLevelEvent.content.users[userB] = 30;
+283 -281
View File
File diff suppressed because it is too large Load Diff
+80 -67
View File
@@ -1,25 +1,33 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var MatrixScheduler = sdk.MatrixScheduler;
var MatrixError = sdk.MatrixError;
var utils = require("../test-utils");
// This file had a function whose name is all caps, which displeases eslint
/* eslint new-cap: "off" */
import 'source-map-support/register';
const q = require("q");
const sdk = require("../..");
const MatrixScheduler = sdk.MatrixScheduler;
const MatrixError = sdk.MatrixError;
const utils = require("../test-utils");
import expect from 'expect';
import lolex from 'lolex';
describe("MatrixScheduler", function() {
var scheduler;
var retryFn, queueFn;
var defer;
var roomId = "!foo:bar";
var eventA = utils.mkMessage({
user: "@alice:bar", room: roomId, event: true
let clock;
let scheduler;
let retryFn;
let queueFn;
let defer;
const roomId = "!foo:bar";
const eventA = utils.mkMessage({
user: "@alice:bar", room: roomId, event: true,
});
var eventB = utils.mkMessage({
user: "@alice:bar", room: roomId, event: true
const eventB = utils.mkMessage({
user: "@alice:bar", room: roomId, event: true,
});
beforeEach(function() {
utils.beforeEach(this);
jasmine.Clock.useMock();
utils.beforeEach(this); // eslint-disable-line no-invalid-this
clock = lolex.install();
scheduler = new MatrixScheduler(function(ev, attempts, err) {
if (retryFn) {
return retryFn(ev, attempts, err);
@@ -36,6 +44,10 @@ describe("MatrixScheduler", function() {
defer = q.defer();
});
afterEach(function() {
clock.uninstall();
});
it("should process events in a queue in a FIFO manner", function(done) {
retryFn = function() {
return 0;
@@ -43,15 +55,14 @@ describe("MatrixScheduler", function() {
queueFn = function() {
return "one_big_queue";
};
var deferA = q.defer();
var deferB = q.defer();
var resolvedA = false;
const deferA = q.defer();
const deferB = q.defer();
let resolvedA = false;
scheduler.setProcessFunction(function(event) {
if (resolvedA) {
expect(event).toEqual(eventB);
return deferB.promise;
}
else {
} else {
expect(event).toEqual(eventA);
return deferA.promise;
}
@@ -68,22 +79,23 @@ describe("MatrixScheduler", function() {
it("should invoke the retryFn on failure and wait the amount of time specified",
function(done) {
var waitTimeMs = 1500;
var retryDefer = q.defer();
const waitTimeMs = 1500;
const retryDefer = q.defer();
retryFn = function() {
retryDefer.resolve();
return waitTimeMs;
};
queueFn = function() { return "yep"; };
queueFn = function() {
return "yep";
};
var procCount = 0;
let procCount = 0;
scheduler.setProcessFunction(function(ev) {
procCount += 1;
if (procCount === 1) {
expect(ev).toEqual(eventA);
return defer.promise;
}
else if (procCount === 2) {
} else if (procCount === 2) {
// don't care about this defer
return q.defer().promise;
}
@@ -95,7 +107,7 @@ describe("MatrixScheduler", function() {
defer.reject({});
retryDefer.promise.done(function() {
expect(procCount).toEqual(1);
jasmine.Clock.tick(waitTimeMs);
clock.tick(waitTimeMs);
expect(procCount).toEqual(2);
done();
});
@@ -109,25 +121,26 @@ describe("MatrixScheduler", function() {
retryFn = function() {
return -1;
};
queueFn = function() { return "yep"; };
queueFn = function() {
return "yep";
};
var deferA = q.defer();
var deferB = q.defer();
var procCount = 0;
const deferA = q.defer();
const deferB = q.defer();
let procCount = 0;
scheduler.setProcessFunction(function(ev) {
procCount += 1;
if (procCount === 1) {
expect(ev).toEqual(eventA);
return deferA.promise;
}
else if (procCount === 2) {
} else if (procCount === 2) {
expect(ev).toEqual(eventB);
return deferB.promise;
}
expect(procCount).toBeLessThan(3);
});
var globalA = scheduler.queueEvent(eventA);
const globalA = scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB);
expect(procCount).toEqual(1);
@@ -145,10 +158,10 @@ describe("MatrixScheduler", function() {
// Expect to have processFn invoked for A&B.
// Resolve A.
// Expect to have processFn invoked for D.
var eventC = utils.mkMessage({user: "@a:bar", room: roomId, event: true});
var eventD = utils.mkMessage({user: "@b:bar", room: roomId, event: true});
const eventC = utils.mkMessage({user: "@a:bar", room: roomId, event: true});
const eventD = utils.mkMessage({user: "@b:bar", room: roomId, event: true});
var buckets = {};
const buckets = {};
buckets[eventA.getId()] = "queue_A";
buckets[eventD.getId()] = "queue_A";
buckets[eventB.getId()] = "queue_B";
@@ -161,12 +174,12 @@ describe("MatrixScheduler", function() {
return buckets[event.getId()];
};
var expectOrder = [
eventA.getId(), eventB.getId(), eventD.getId()
const expectOrder = [
eventA.getId(), eventB.getId(), eventD.getId(),
];
var deferA = q.defer();
const deferA = q.defer();
scheduler.setProcessFunction(function(event) {
var id = expectOrder.shift();
const id = expectOrder.shift();
expect(id).toEqual(event.getId());
if (expectOrder.length === 0) {
done();
@@ -182,7 +195,7 @@ describe("MatrixScheduler", function() {
setTimeout(function() {
deferA.resolve({});
}, 1000);
jasmine.Clock.tick(1000);
clock.tick(1000);
});
describe("queueEvent", function() {
@@ -197,9 +210,9 @@ describe("MatrixScheduler", function() {
queueFn = function() {
return "yep";
};
var prom = scheduler.queueEvent(eventA);
expect(prom).toBeDefined();
expect(prom.then).toBeDefined();
const prom = scheduler.queueEvent(eventA);
expect(prom).toBeTruthy();
expect(prom.then).toBeTruthy();
});
});
@@ -208,14 +221,14 @@ describe("MatrixScheduler", function() {
queueFn = function() {
return null;
};
expect(scheduler.getQueueForEvent(eventA)).toBeNull();
expect(scheduler.getQueueForEvent(eventA)).toBe(null);
});
it("should return null if the mapped queue doesn't exist", function() {
queueFn = function() {
return "yep";
};
expect(scheduler.getQueueForEvent(eventA)).toBeNull();
expect(scheduler.getQueueForEvent(eventA)).toBe(null);
});
it("should return a list of events in the queue and modifications to" +
@@ -225,15 +238,15 @@ describe("MatrixScheduler", function() {
};
scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB);
var queue = scheduler.getQueueForEvent(eventA);
const queue = scheduler.getQueueForEvent(eventA);
expect(queue.length).toEqual(2);
expect(queue).toEqual([eventA, eventB]);
// modify the queue
var eventC = utils.mkMessage(
{user: "@a:bar", room: roomId, event: true}
const eventC = utils.mkMessage(
{user: "@a:bar", room: roomId, event: true},
);
queue.push(eventC);
var queueAgain = scheduler.getQueueForEvent(eventA);
const queueAgain = scheduler.getQueueForEvent(eventA);
expect(queueAgain.length).toEqual(2);
});
@@ -244,9 +257,9 @@ describe("MatrixScheduler", function() {
};
scheduler.queueEvent(eventA);
scheduler.queueEvent(eventB);
var queue = scheduler.getQueueForEvent(eventA);
const queue = scheduler.getQueueForEvent(eventA);
queue[1].event.content.body = "foo";
var queueAgain = scheduler.getQueueForEvent(eventA);
const queueAgain = scheduler.getQueueForEvent(eventA);
expect(queueAgain[1].event.content.body).toEqual("foo");
});
});
@@ -280,7 +293,7 @@ describe("MatrixScheduler", function() {
queueFn = function() {
return "yep";
};
var procCount = 0;
let procCount = 0;
scheduler.queueEvent(eventA);
scheduler.setProcessFunction(function(ev) {
procCount += 1;
@@ -294,7 +307,7 @@ describe("MatrixScheduler", function() {
queueFn = function() {
return "yep";
};
var procCount = 0;
let procCount = 0;
scheduler.setProcessFunction(function(ev) {
procCount += 1;
return defer.promise;
@@ -308,41 +321,41 @@ describe("MatrixScheduler", function() {
expect(MatrixScheduler.QUEUE_MESSAGES(eventA)).toEqual("message");
expect(MatrixScheduler.QUEUE_MESSAGES(
utils.mkMembership({
user: "@alice:bar", room: roomId, mship: "join", event: true
})
user: "@alice:bar", room: roomId, mship: "join", event: true,
}),
)).toEqual(null);
});
});
describe("RETRY_BACKOFF_RATELIMIT", function() {
it("should wait at least the time given on M_LIMIT_EXCEEDED", function() {
var res = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
const res = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 1, new MatrixError({
errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 5000
})
errcode: "M_LIMIT_EXCEEDED", retry_after_ms: 5000,
}),
);
expect(res >= 500).toBe(true, "Didn't wait long enough.");
});
it("should give up after 5 attempts", function() {
var res = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 5, {}
const res = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 5, {},
);
expect(res).toBe(-1, "Didn't give up.");
});
it("should do exponential backoff", function() {
expect(MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 1, {}
eventA, 1, {},
)).toEqual(2000);
expect(MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 2, {}
eventA, 2, {},
)).toEqual(4000);
expect(MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 3, {}
eventA, 3, {},
)).toEqual(8000);
expect(MatrixScheduler.RETRY_BACKOFF_RATELIMIT(
eventA, 4, {}
eventA, 4, {},
)).toEqual(16000);
});
});
+356
View File
@@ -0,0 +1,356 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import 'source-map-support/register';
import utils from "../test-utils";
import sdk from "../..";
import expect from 'expect';
const SyncAccumulator = sdk.SyncAccumulator;
describe("SyncAccumulator", function() {
let sa;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
sa = new SyncAccumulator({
maxTimelineEntries: 10,
});
});
it("should return the same /sync response if accumulated exactly once", () => {
// technically cheating since we also cheekily pre-populate keys we
// know that the sync accumulator will pre-populate.
// It isn't 100% transitive.
const res = {
next_batch: "abc",
rooms: {
invite: {},
leave: {},
join: {
"!foo:bar": {
account_data: { events: [] },
ephemeral: { events: [] },
unread_notifications: {},
state: {
events: [
member("alice", "join"),
member("bob", "join"),
],
},
timeline: {
events: [msg("alice", "hi")],
prev_batch: "something",
},
},
},
},
};
sa.accumulate(res);
const output = sa.getJSON();
expect(output.nextBatch).toEqual(res.next_batch);
expect(output.roomsData).toEqual(res.rooms);
});
it("should prune the timeline to the oldest prev_batch within the limit", () => {
// maxTimelineEntries is 10 so we should get back all
// 10 timeline messages with a prev_batch of "pinned_to_1"
sa.accumulate(syncSkeleton({
state: { events: [member("alice", "join")] },
timeline: {
events: [
msg("alice", "1"),
msg("alice", "2"),
msg("alice", "3"),
msg("alice", "4"),
msg("alice", "5"),
msg("alice", "6"),
msg("alice", "7"),
],
prev_batch: "pinned_to_1",
},
}));
sa.accumulate(syncSkeleton({
state: { events: [] },
timeline: {
events: [
msg("alice", "8"),
],
prev_batch: "pinned_to_8",
},
}));
sa.accumulate(syncSkeleton({
state: { events: [] },
timeline: {
events: [
msg("alice", "9"),
msg("alice", "10"),
],
prev_batch: "pinned_to_10",
},
}));
let output = sa.getJSON().roomsData.join["!foo:bar"];
expect(output.timeline.events.length).toEqual(10);
output.timeline.events.forEach((e, i) => {
expect(e.content.body).toEqual(""+(i+1));
});
expect(output.timeline.prev_batch).toEqual("pinned_to_1");
// accumulate more messages. Now it can't have a prev_batch of "pinned to 1"
// AND give us <= 10 messages without losing messages in-between.
// It should try to find the oldest prev_batch which still fits into 10
// messages, which is "pinned to 8".
sa.accumulate(syncSkeleton({
state: { events: [] },
timeline: {
events: [
msg("alice", "11"),
msg("alice", "12"),
msg("alice", "13"),
msg("alice", "14"),
msg("alice", "15"),
msg("alice", "16"),
msg("alice", "17"),
],
prev_batch: "pinned_to_11",
},
}));
output = sa.getJSON().roomsData.join["!foo:bar"];
expect(output.timeline.events.length).toEqual(10);
output.timeline.events.forEach((e, i) => {
expect(e.content.body).toEqual(""+(i+8));
});
expect(output.timeline.prev_batch).toEqual("pinned_to_8");
});
it("should remove the stored timeline on limited syncs", () => {
sa.accumulate(syncSkeleton({
state: { events: [member("alice", "join")] },
timeline: {
events: [
msg("alice", "1"),
msg("alice", "2"),
msg("alice", "3"),
],
prev_batch: "pinned_to_1",
},
}));
// some time passes and now we get a limited sync
sa.accumulate(syncSkeleton({
state: { events: [] },
timeline: {
limited: true,
events: [
msg("alice", "51"),
msg("alice", "52"),
msg("alice", "53"),
],
prev_batch: "pinned_to_51",
},
}));
const output = sa.getJSON().roomsData.join["!foo:bar"];
expect(output.timeline.events.length).toEqual(3);
output.timeline.events.forEach((e, i) => {
expect(e.content.body).toEqual(""+(i+51));
});
expect(output.timeline.prev_batch).toEqual("pinned_to_51");
});
it("should drop typing notifications", () => {
const res = syncSkeleton({
ephemeral: {
events: [{
type: "m.typing",
content: {
user_ids: ["@alice:localhost"],
},
room_id: "!foo:bar",
}],
},
});
sa.accumulate(res);
expect(
sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events.length,
).toEqual(0);
});
it("should clobber account data based on event type", () => {
const acc1 = {
type: "favourite.food",
content: {
food: "banana",
},
};
const acc2 = {
type: "favourite.food",
content: {
food: "apple",
},
};
sa.accumulate(syncSkeleton({
account_data: {
events: [acc1],
},
}));
sa.accumulate(syncSkeleton({
account_data: {
events: [acc2],
},
}));
expect(
sa.getJSON().roomsData.join["!foo:bar"].account_data.events.length,
).toEqual(1);
expect(
sa.getJSON().roomsData.join["!foo:bar"].account_data.events[0],
).toEqual(acc2);
});
it("should clobber global account data based on event type", () => {
const acc1 = {
type: "favourite.food",
content: {
food: "banana",
},
};
const acc2 = {
type: "favourite.food",
content: {
food: "apple",
},
};
sa.accumulate({
account_data: {
events: [acc1],
},
});
sa.accumulate({
account_data: {
events: [acc2],
},
});
expect(
sa.getJSON().accountData.length,
).toEqual(1);
expect(
sa.getJSON().accountData[0],
).toEqual(acc2);
});
it("should accumulate read receipts", () => {
const receipt1 = {
type: "m.receipt",
room_id: "!foo:bar",
content: {
"$event1:localhost": {
"m.read": {
"@alice:localhost": { ts: 1 },
"@bob:localhost": { ts: 2 },
},
"some.other.receipt.type": {
"@should_be_ignored:localhost": { key: "val" },
},
},
},
};
const receipt2 = {
type: "m.receipt",
room_id: "!foo:bar",
content: {
"$event2:localhost": {
"m.read": {
"@bob:localhost": { ts: 2 }, // clobbers event1 receipt
"@charlie:localhost": { ts: 3 },
},
},
},
};
sa.accumulate(syncSkeleton({
ephemeral: {
events: [receipt1],
},
}));
sa.accumulate(syncSkeleton({
ephemeral: {
events: [receipt2],
},
}));
expect(
sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events.length,
).toEqual(1);
expect(
sa.getJSON().roomsData.join["!foo:bar"].ephemeral.events[0],
).toEqual({
type: "m.receipt",
room_id: "!foo:bar",
content: {
"$event1:localhost": {
"m.read": {
"@alice:localhost": { ts: 1 },
},
},
"$event2:localhost": {
"m.read": {
"@bob:localhost": { ts: 2 },
"@charlie:localhost": { ts: 3 },
},
},
},
});
});
});
function syncSkeleton(joinObj) {
joinObj = joinObj || {};
return {
next_batch: "abc",
rooms: {
join: {
"!foo:bar": joinObj,
},
},
};
}
function msg(localpart, text) {
return {
content: {
body: text,
},
origin_server_ts: 123456789,
sender: "@" + localpart + ":localhost",
type: "m.room.message",
};
}
function member(localpart, membership) {
return {
content: {
membership: membership,
},
origin_server_ts: 123456789,
state_key: "@" + localpart + ":localhost",
sender: "@" + localpart + ":localhost",
type: "m.room.member",
};
}
+100 -87
View File
@@ -1,28 +1,36 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var EventTimeline = sdk.EventTimeline;
var TimelineWindow = sdk.TimelineWindow;
var TimelineIndex = require("../../lib/timeline-window").TimelineIndex;
import 'source-map-support/register';
const q = require("q");
const sdk = require("../..");
const EventTimeline = sdk.EventTimeline;
const TimelineWindow = sdk.TimelineWindow;
const TimelineIndex = require("../../lib/timeline-window").TimelineIndex;
var utils = require("../test-utils");
const utils = require("../test-utils");
import expect from 'expect';
var ROOM_ID = "roomId";
var USER_ID = "userId";
const ROOM_ID = "roomId";
const USER_ID = "userId";
/*
* create a timeline with a bunch (default 3) events.
* baseIndex is 1 by default.
*/
function createTimeline(numEvents, baseIndex) {
if (numEvents === undefined) { numEvents = 3; }
if (baseIndex === undefined) { baseIndex = 1; }
if (numEvents === undefined) {
numEvents = 3;
}
if (baseIndex === undefined) {
baseIndex = 1;
}
// XXX: this is a horrid hack
var timelineSet = { room: { roomId: ROOM_ID }};
timelineSet.room.getUnfilteredTimelineSet = function() { return timelineSet; };
const timelineSet = { room: { roomId: ROOM_ID }};
timelineSet.room.getUnfilteredTimelineSet = function() {
return timelineSet;
};
var timeline = new EventTimeline(timelineSet);
const timeline = new EventTimeline(timelineSet);
// add the events after the baseIndex first
addEventsToTimeline(timeline, numEvents - baseIndex, false);
@@ -35,12 +43,12 @@ function createTimeline(numEvents, baseIndex) {
}
function addEventsToTimeline(timeline, numEvents, atStart) {
for (var i = 0; i < numEvents; i++) {
for (let i = 0; i < numEvents; i++) {
timeline.addEvent(
utils.mkMessage({
room: ROOM_ID, user: USER_ID,
event: true,
}), atStart
}), atStart,
);
}
}
@@ -50,8 +58,8 @@ function addEventsToTimeline(timeline, numEvents, atStart) {
* create a pair of linked timelines
*/
function createLinkedTimelines() {
var tl1 = createTimeline();
var tl2 = createTimeline();
const tl1 = createTimeline();
const tl2 = createTimeline();
tl1.setNeighbouringTimeline(tl2, EventTimeline.FORWARDS);
tl2.setNeighbouringTimeline(tl1, EventTimeline.BACKWARDS);
return [tl1, tl2];
@@ -60,46 +68,47 @@ function createLinkedTimelines() {
describe("TimelineIndex", function() {
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe("minIndex", function() {
it("should return the min index relative to BaseIndex", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
const timelineIndex = new TimelineIndex(createTimeline(), 0);
expect(timelineIndex.minIndex()).toEqual(-1);
});
});
describe("maxIndex", function() {
it("should return the max index relative to BaseIndex", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
const timelineIndex = new TimelineIndex(createTimeline(), 0);
expect(timelineIndex.maxIndex()).toEqual(2);
});
});
describe("advance", function() {
it("should advance up to the end of the timeline", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
var result = timelineIndex.advance(3);
const timelineIndex = new TimelineIndex(createTimeline(), 0);
const result = timelineIndex.advance(3);
expect(result).toEqual(2);
expect(timelineIndex.index).toEqual(2);
});
it("should retreat back to the start of the timeline", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
var result = timelineIndex.advance(-2);
const timelineIndex = new TimelineIndex(createTimeline(), 0);
const result = timelineIndex.advance(-2);
expect(result).toEqual(-1);
expect(timelineIndex.index).toEqual(-1);
});
it("should advance into the next timeline", function() {
var timelines = createLinkedTimelines();
var tl1 = timelines[0], tl2 = timelines[1];
const timelines = createLinkedTimelines();
const tl1 = timelines[0];
const tl2 = timelines[1];
// initialise the index pointing at the end of the first timeline
var timelineIndex = new TimelineIndex(tl1, 2);
const timelineIndex = new TimelineIndex(tl1, 2);
var result = timelineIndex.advance(1);
const result = timelineIndex.advance(1);
expect(result).toEqual(1);
expect(timelineIndex.timeline).toBe(tl2);
@@ -110,14 +119,15 @@ describe("TimelineIndex", function() {
});
it("should retreat into the previous timeline", function() {
var timelines = createLinkedTimelines();
var tl1 = timelines[0], tl2 = timelines[1];
const timelines = createLinkedTimelines();
const tl1 = timelines[0];
const tl2 = timelines[1];
// initialise the index pointing at the start of the second
// timeline
var timelineIndex = new TimelineIndex(tl2, -1);
const timelineIndex = new TimelineIndex(tl2, -1);
var result = timelineIndex.advance(-1);
const result = timelineIndex.advance(-1);
expect(result).toEqual(-1);
expect(timelineIndex.timeline).toBe(tl1);
expect(timelineIndex.index).toEqual(1);
@@ -126,8 +136,8 @@ describe("TimelineIndex", function() {
describe("retreat", function() {
it("should retreat up to the start of the timeline", function() {
var timelineIndex = new TimelineIndex(createTimeline(), 0);
var result = timelineIndex.retreat(2);
const timelineIndex = new TimelineIndex(createTimeline(), 0);
const result = timelineIndex.retreat(2);
expect(result).toEqual(1);
expect(timelineIndex.index).toEqual(-1);
});
@@ -140,7 +150,8 @@ describe("TimelineWindow", function() {
* create a dummy eventTimelineSet and client, and a TimelineWindow
* attached to them.
*/
var timelineSet, client;
let timelineSet;
let client;
function createWindow(timeline, opts) {
timelineSet = {};
client = {};
@@ -153,53 +164,55 @@ describe("TimelineWindow", function() {
}
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe("load", function() {
it("should initialise from the live timeline", function(done) {
var liveTimeline = createTimeline();
var room = {};
room.getLiveTimeline = function() { return liveTimeline; };
const liveTimeline = createTimeline();
const room = {};
room.getLiveTimeline = function() {
return liveTimeline;
};
var timelineWindow = new TimelineWindow(undefined, room);
const timelineWindow = new TimelineWindow(undefined, room);
timelineWindow.load(undefined, 2).then(function() {
var expectedEvents = liveTimeline.getEvents().slice(1);
const expectedEvents = liveTimeline.getEvents().slice(1);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
});
it("should initialise from a specific event", function(done) {
var timeline = createTimeline();
var eventId = timeline.getEvents()[1].getId();
const timeline = createTimeline();
const eventId = timeline.getEvents()[1].getId();
var timelineSet = {};
var client = {};
const timelineSet = {};
const client = {};
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineSet0).toBe(timelineSet);
expect(eventId0).toEqual(eventId);
return q(timeline);
};
var timelineWindow = new TimelineWindow(client, timelineSet);
const timelineWindow = new TimelineWindow(client, timelineSet);
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
});
it("canPaginate should return false until load has returned",
function(done) {
var timeline = createTimeline();
const timeline = createTimeline();
timeline.setPaginationToken("toktok1", EventTimeline.BACKWARDS);
timeline.setPaginationToken("toktok2", EventTimeline.FORWARDS);
var eventId = timeline.getEvents()[1].getId();
const eventId = timeline.getEvents()[1].getId();
var timelineSet = {};
var client = {};
const timelineSet = {};
const client = {};
var timelineWindow = new TimelineWindow(client, timelineSet);
const timelineWindow = new TimelineWindow(client, timelineSet);
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
@@ -210,7 +223,7 @@ describe("TimelineWindow", function() {
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
@@ -223,12 +236,12 @@ describe("TimelineWindow", function() {
describe("pagination", function() {
it("should be able to advance across the initial timeline",
function(done) {
var timeline = createTimeline();
var eventId = timeline.getEvents()[1].getId();
var timelineWindow = createWindow(timeline);
const timeline = createTimeline();
const eventId = timeline.getEvents()[1].getId();
const timelineWindow = createWindow(timeline);
timelineWindow.load(eventId, 1).then(function() {
var expectedEvents = [timeline.getEvents()[1]];
const expectedEvents = [timeline.getEvents()[1]];
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
@@ -239,7 +252,7 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = timeline.getEvents().slice(1);
const expectedEvents = timeline.getEvents().slice(1);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
@@ -254,7 +267,7 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = timeline.getEvents();
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
@@ -268,12 +281,12 @@ describe("TimelineWindow", function() {
});
it("should advance into next timeline", function(done) {
var tls = createLinkedTimelines();
var eventId = tls[0].getEvents()[1].getId();
var timelineWindow = createWindow(tls[0], {windowLimit: 5});
const tls = createLinkedTimelines();
const eventId = tls[0].getEvents()[1].getId();
const timelineWindow = createWindow(tls[0], {windowLimit: 5});
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = tls[0].getEvents();
const expectedEvents = tls[0].getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
@@ -284,7 +297,7 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = tls[0].getEvents()
const expectedEvents = tls[0].getEvents()
.concat(tls[1].getEvents().slice(0, 2));
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
@@ -298,7 +311,7 @@ describe("TimelineWindow", function() {
expect(success).toBe(true);
// the windowLimit should have made us drop an event from
// tls[0]
var expectedEvents = tls[0].getEvents().slice(1)
const expectedEvents = tls[0].getEvents().slice(1)
.concat(tls[1].getEvents());
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
@@ -313,12 +326,12 @@ describe("TimelineWindow", function() {
});
it("should retreat into previous timeline", function(done) {
var tls = createLinkedTimelines();
var eventId = tls[1].getEvents()[1].getId();
var timelineWindow = createWindow(tls[1], {windowLimit: 5});
const tls = createLinkedTimelines();
const eventId = tls[1].getEvents()[1].getId();
const timelineWindow = createWindow(tls[1], {windowLimit: 5});
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = tls[1].getEvents();
const expectedEvents = tls[1].getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
@@ -329,7 +342,7 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = tls[0].getEvents().slice(1, 3)
const expectedEvents = tls[0].getEvents().slice(1, 3)
.concat(tls[1].getEvents());
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
@@ -343,7 +356,7 @@ describe("TimelineWindow", function() {
expect(success).toBe(true);
// the windowLimit should have made us drop an event from
// tls[1]
var expectedEvents = tls[0].getEvents()
const expectedEvents = tls[0].getEvents()
.concat(tls[1].getEvents().slice(0, 2));
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
@@ -358,11 +371,11 @@ describe("TimelineWindow", function() {
});
it("should make forward pagination requests", function(done) {
var timeline = createTimeline();
const timeline = createTimeline();
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
var timelineWindow = createWindow(timeline, {windowLimit: 5});
var eventId = timeline.getEvents()[1].getId();
const timelineWindow = createWindow(timeline, {windowLimit: 5});
const eventId = timeline.getEvents()[1].getId();
client.paginateEventTimeline = function(timeline0, opts) {
expect(timeline0).toBe(timeline);
@@ -374,7 +387,7 @@ describe("TimelineWindow", function() {
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
@@ -384,18 +397,18 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = timeline.getEvents().slice(0, 5);
const expectedEvents = timeline.getEvents().slice(0, 5);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
});
it("should make backward pagination requests", function(done) {
var timeline = createTimeline();
const timeline = createTimeline();
timeline.setPaginationToken("toktok", EventTimeline.BACKWARDS);
var timelineWindow = createWindow(timeline, {windowLimit: 5});
var eventId = timeline.getEvents()[1].getId();
const timelineWindow = createWindow(timeline, {windowLimit: 5});
const eventId = timeline.getEvents()[1].getId();
client.paginateEventTimeline = function(timeline0, opts) {
expect(timeline0).toBe(timeline);
@@ -407,7 +420,7 @@ describe("TimelineWindow", function() {
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
@@ -417,20 +430,20 @@ describe("TimelineWindow", function() {
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
var expectedEvents = timeline.getEvents().slice(1, 6);
const expectedEvents = timeline.getEvents().slice(1, 6);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
}).catch(utils.failTest).done(done);
});
it("should limit the number of unsuccessful pagination requests",
function(done) {
var timeline = createTimeline();
const timeline = createTimeline();
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
var timelineWindow = createWindow(timeline, {windowLimit: 5});
var eventId = timeline.getEvents()[1].getId();
const timelineWindow = createWindow(timeline, {windowLimit: 5});
const eventId = timeline.getEvents()[1].getId();
var paginateCount = 0;
let paginateCount = 0;
client.paginateEventTimeline = function(timeline0, opts) {
expect(timeline0).toBe(timeline);
expect(opts.backwards).toBe(false);
@@ -440,7 +453,7 @@ describe("TimelineWindow", function() {
};
timelineWindow.load(eventId, 3).then(function() {
var expectedEvents = timeline.getEvents();
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
@@ -451,7 +464,7 @@ describe("TimelineWindow", function() {
}).then(function(success) {
expect(success).toBe(false);
expect(paginateCount).toEqual(3);
var expectedEvents = timeline.getEvents().slice(0, 3);
const expectedEvents = timeline.getEvents().slice(0, 3);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
+15 -12
View File
@@ -1,30 +1,33 @@
"use strict";
var sdk = require("../..");
var User = sdk.User;
var utils = require("../test-utils");
import 'source-map-support/register';
const sdk = require("../..");
const User = sdk.User;
const utils = require("../test-utils");
import expect from 'expect';
describe("User", function() {
var userId = "@alice:bar";
var user;
const userId = "@alice:bar";
let user;
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
user = new User(userId);
});
describe("setPresenceEvent", function() {
var event = utils.mkEvent({
const event = utils.mkEvent({
type: "m.presence", content: {
presence: "online",
user_id: userId,
displayname: "Alice",
last_active_ago: 1085,
avatar_url: "mxc://foo/bar"
}, event: true
avatar_url: "mxc://foo/bar",
}, event: true,
});
it("should emit 'User.displayName' if the display name changes", function() {
var emitCount = 0;
let emitCount = 0;
user.on("User.displayName", function(ev, usr) {
emitCount += 1;
});
@@ -35,7 +38,7 @@ describe("User", function() {
});
it("should emit 'User.avatarUrl' if the avatar URL changes", function() {
var emitCount = 0;
let emitCount = 0;
user.on("User.avatarUrl", function(ev, usr) {
emitCount += 1;
});
@@ -46,7 +49,7 @@ describe("User", function() {
});
it("should emit 'User.presence' if the presence changes", function() {
var emitCount = 0;
let emitCount = 0;
user.on("User.presence", function(ev, usr) {
emitCount += 1;
});
+86 -55
View File
@@ -1,40 +1,43 @@
"use strict";
var utils = require("../../lib/utils");
var testUtils = require("../test-utils");
import 'source-map-support/register';
const utils = require("../../lib/utils");
const testUtils = require("../test-utils");
import expect from 'expect';
describe("utils", function() {
beforeEach(function() {
testUtils.beforeEach(this);
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe("encodeParams", function() {
it("should url encode and concat with &s", function() {
var params = {
const params = {
foo: "bar",
baz: "beer@"
baz: "beer@",
};
expect(utils.encodeParams(params)).toEqual(
"foo=bar&baz=beer%40"
"foo=bar&baz=beer%40",
);
});
});
describe("encodeUri", function() {
it("should replace based on object keys and url encode", function() {
var path = "foo/bar/%something/%here";
var vals = {
const path = "foo/bar/%something/%here";
const vals = {
"%something": "baz",
"%here": "beer@"
"%here": "beer@",
};
expect(utils.encodeUri(path, vals)).toEqual(
"foo/bar/baz/beer%40"
"foo/bar/baz/beer%40",
);
});
});
describe("forEach", function() {
it("should be invoked for each element", function() {
var arr = [];
const arr = [];
utils.forEach([55, 66, 77], function(element) {
arr.push(element);
});
@@ -44,38 +47,50 @@ describe("utils", function() {
describe("findElement", function() {
it("should find only 1 element if there is a match", function() {
var matchFn = function() { return true; };
var arr = [55, 66, 77];
const matchFn = function() {
return true;
};
const arr = [55, 66, 77];
expect(utils.findElement(arr, matchFn)).toEqual(55);
});
it("should be able to find in reverse order", function() {
var matchFn = function() { return true; };
var arr = [55, 66, 77];
const matchFn = function() {
return true;
};
const arr = [55, 66, 77];
expect(utils.findElement(arr, matchFn, true)).toEqual(77);
});
it("should find nothing if the function never returns true", function() {
var matchFn = function() { return false; };
var arr = [55, 66, 77];
const matchFn = function() {
return false;
};
const arr = [55, 66, 77];
expect(utils.findElement(arr, matchFn)).toBeFalsy();
});
});
describe("removeElement", function() {
it("should remove only 1 element if there is a match", function() {
var matchFn = function() { return true; };
var arr = [55, 66, 77];
const matchFn = function() {
return true;
};
const arr = [55, 66, 77];
utils.removeElement(arr, matchFn);
expect(arr).toEqual([66, 77]);
});
it("should be able to remove in reverse order", function() {
var matchFn = function() { return true; };
var arr = [55, 66, 77];
const matchFn = function() {
return true;
};
const arr = [55, 66, 77];
utils.removeElement(arr, matchFn, true);
expect(arr).toEqual([55, 66]);
});
it("should remove nothing if the function never returns true", function() {
var matchFn = function() { return false; };
var arr = [55, 66, 77];
const matchFn = function() {
return false;
};
const arr = [55, 66, 77];
utils.removeElement(arr, matchFn);
expect(arr).toEqual(arr);
});
@@ -92,7 +107,7 @@ describe("utils", function() {
expect(utils.isFunction(555)).toBe(false);
expect(utils.isFunction(function() {})).toBe(true);
var s = { foo: function() {} };
const s = { foo: function() {} };
expect(utils.isFunction(s.foo)).toBe(true);
});
});
@@ -113,30 +128,42 @@ describe("utils", function() {
describe("checkObjectHasKeys", function() {
it("should throw for missing keys", function() {
expect(function() { utils.checkObjectHasKeys({}, ["foo"]); }).toThrow();
expect(function() { utils.checkObjectHasKeys({
foo: "bar"
}, ["foo"]); }).not.toThrow();
expect(function() {
utils.checkObjectHasKeys({}, ["foo"]);
}).toThrow();
expect(function() {
utils.checkObjectHasKeys({
foo: "bar",
}, ["foo"]);
}).toNotThrow();
});
});
describe("checkObjectHasNoAdditionalKeys", function() {
it("should throw for extra keys", function() {
expect(function() { utils.checkObjectHasNoAdditionalKeys({
foo: "bar",
baz: 4
}, ["foo"]); }).toThrow();
expect(function() {
utils.checkObjectHasNoAdditionalKeys({
foo: "bar",
baz: 4,
}, ["foo"]);
}).toThrow();
expect(function() { utils.checkObjectHasNoAdditionalKeys({
foo: "bar"
}, ["foo"]); }).not.toThrow();
expect(function() {
utils.checkObjectHasNoAdditionalKeys({
foo: "bar",
}, ["foo"]);
}).toNotThrow();
});
});
describe("deepCompare", function() {
var assert = {
isTrue: function(x) { expect(x).toBe(true); },
isFalse: function(x) { expect(x).toBe(false); },
const assert = {
isTrue: function(x) {
expect(x).toBe(true);
},
isFalse: function(x) {
expect(x).toBe(false);
},
};
it("should handle primitives", function() {
@@ -150,7 +177,7 @@ describe("utils", function() {
it("should handle regexps", function() {
assert.isTrue(utils.deepCompare(/abc/, /abc/));
assert.isFalse(utils.deepCompare(/abc/, /123/));
var r = /abc/;
const r = /abc/;
assert.isTrue(utils.deepCompare(r, r));
});
@@ -192,8 +219,12 @@ describe("utils", function() {
// no two different function is equal really, they capture their
// context variables so even if they have same toString(), they
// won't have same functionality
var func = function(x) { return true; };
var func2 = function(x) { return true; };
const func = function(x) {
return true;
};
const func2 = function(x) {
return true;
};
assert.isTrue(utils.deepCompare(func, func));
assert.isFalse(utils.deepCompare(func, func2));
assert.isTrue(utils.deepCompare({ a: { b: func } }, { a: { b: func } }));
@@ -203,58 +234,58 @@ describe("utils", function() {
describe("extend", function() {
var SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
const SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
it("should extend", function() {
var target = {
const target = {
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
};
var merged = {
const merged = {
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
"newprop": "new",
};
var source_orig = JSON.stringify(SOURCE);
const sourceOrig = JSON.stringify(SOURCE);
utils.extend(target, SOURCE);
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
// check the originial wasn't modified
expect(JSON.stringify(SOURCE)).toEqual(source_orig);
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
});
it("should ignore null", function() {
var target = {
const target = {
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
};
var merged = {
const merged = {
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
"newprop": "new",
};
var source_orig = JSON.stringify(SOURCE);
const sourceOrig = JSON.stringify(SOURCE);
utils.extend(target, null, SOURCE);
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
// check the originial wasn't modified
expect(JSON.stringify(SOURCE)).toEqual(source_orig);
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
});
it("should handle properties created with defineProperties", function() {
var source = Object.defineProperties({}, {
const source = Object.defineProperties({}, {
"enumerableProp": {
get: function() {
return true;
},
enumerable: true
enumerable: true,
},
"nonenumerableProp": {
get: function() {
return true;
}
}
},
},
});
var target = {};
const target = {};
utils.extend(target, source);
expect(target.enumerableProp).toBe(true);
expect(target.nonenumerableProp).toBe(undefined);
-580
View File
@@ -1,580 +0,0 @@
"use strict";
var sdk = require("../..");
var WebStorageStore = sdk.WebStorageStore;
var Room = sdk.Room;
var User = sdk.User;
var utils = require("../test-utils");
var MockStorageApi = require("../MockStorageApi");
describe("WebStorageStore", function() {
var store, room;
var roomId = "!foo:bar";
var userId = "@alice:bar";
var mockStorageApi;
var batchNum = 3;
// web storage api keys
var prefix = "room_" + roomId + "_timeline_";
var stateKeyName = "room_" + roomId + "_state";
// stored state events
var stateEventMap = {
"m.room.member": {},
"m.room.name": {}
};
stateEventMap["m.room.member"][userId] = utils.mkMembership(
{user: userId, room: roomId, mship: "join"}
);
stateEventMap["m.room.name"][""] = utils.mkEvent(
{user: userId, room: roomId, type: "m.room.name",
content: {
name: "foo"
}}
);
beforeEach(function() {
utils.beforeEach(this);
mockStorageApi = new MockStorageApi();
store = new WebStorageStore(mockStorageApi, batchNum);
room = new Room(roomId);
});
describe("constructor", function() {
it("should throw if the WebStorage API functions are missing", function() {
expect(function() {
store = new WebStorageStore({}, 5);
}).toThrow();
expect(function() {
mockStorageApi.length = undefined;
store = new WebStorageStore(mockStorageApi, 5);
}).toThrow();
});
});
describe("syncToken", function() {
it("get: should return the token from the store", function() {
var token = "flibble";
store.setSyncToken(token);
expect(store.getSyncToken()).toEqual(token);
expect(mockStorageApi.length).toEqual(1);
});
it("get: should return null if the token does not exist", function() {
expect(store.getSyncToken()).toEqual(null);
expect(mockStorageApi.length).toEqual(0);
});
});
describe("storeRoom", function() {
it("should persist the room state correctly", function() {
var stateEvents = [
utils.mkEvent({
event: true, type: "m.room.create", user: userId, room: roomId,
content: {
creator: userId
}
}),
utils.mkMembership({
event: true, user: userId, room: roomId, mship: "join"
})
];
room.currentState.setStateEvents(stateEvents);
store.storeRoom(room);
var storedEvents = getItem(mockStorageApi,
"room_" + roomId + "_state"
).events;
expect(storedEvents["m.room.create"][""]).toEqual(stateEvents[0].event);
});
it("should persist timeline events correctly", function() {
var timelineEvents = [];
var entries = batchNum + batchNum - 1;
var i = 0;
for (i = 0; i < entries; i++) {
timelineEvents.push(
utils.mkMessage({room: roomId, user: userId, event: true})
);
}
room.timeline = timelineEvents;
store.storeRoom(room);
expect(getItem(mockStorageApi, prefix + "-1")).toBe(null);
expect(getItem(mockStorageApi, prefix + "2")).toBe(null);
expect(getItem(mockStorageApi, prefix + "live")).toBe(null);
var timeline0 = getItem(mockStorageApi, prefix + "0");
var timeline1 = getItem(mockStorageApi, prefix + "1");
expect(timeline0.length).toEqual(batchNum);
expect(timeline1.length).toEqual(batchNum - 1);
for (i = 0; i < batchNum; i++) {
expect(timeline0[i]).toEqual(timelineEvents[i].event);
if ((i + batchNum) < timelineEvents.length) {
expect(timeline1[i]).toEqual(timelineEvents[i + batchNum].event);
}
}
});
it("should persist timeline events in one bucket if batchNum=0", function() {
store = new WebStorageStore(mockStorageApi, 0);
var timelineEvents = [];
var entries = batchNum + batchNum - 1;
var i = 0;
for (i = 0; i < entries; i++) {
timelineEvents.push(
utils.mkMessage({room: roomId, user: userId, event: true})
);
}
room.timeline = timelineEvents;
store.storeRoom(room);
expect(getItem(mockStorageApi, prefix + "-1")).toBe(null);
expect(getItem(mockStorageApi, prefix + "1")).toBe(null);
expect(getItem(mockStorageApi, prefix + "live")).toBe(null);
var timeline = getItem(mockStorageApi, prefix + "0");
expect(timeline.length).toEqual(timelineEvents.length);
for (i = 0; i < timeline.length; i++) {
expect(timeline[i]).toEqual(
timelineEvents[i].event
);
}
});
});
describe("getRoom", function() {
// stored timeline events
var timeline0, timeline1, i;
beforeEach(function() {
timeline0 = [];
timeline1 = [];
for (i = 0; i < batchNum; i++) {
timeline1[i] = utils.mkMessage({user: userId, room: roomId});
if (i !== (batchNum - 1)) { // miss last one
timeline0[i] = utils.mkMessage({user: userId, room: roomId});
}
}
});
it("should reconstruct room state", function() {
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
var storedRoom = store.getRoom(roomId);
expect(
storedRoom.currentState.getStateEvents("m.room.name", "").event
).toEqual(stateEventMap["m.room.name"][""]);
expect(
storedRoom.currentState.getStateEvents("m.room.member", userId).event
).toEqual(stateEventMap["m.room.member"][userId]);
});
it("should reconstruct old room state", function() {
var inviteEvent = utils.mkMembership({
user: userId, room: roomId, mship: "invite"
});
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", [inviteEvent]);
var storedRoom = store.getRoom(roomId);
expect(
storedRoom.currentState.getStateEvents("m.room.member", userId).event
).toEqual(stateEventMap["m.room.member"][userId]);
expect(
storedRoom.oldState.getStateEvents("m.room.member", userId).event
).toEqual(inviteEvent);
});
it("should reconstruct the room timeline", function() {
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", timeline0);
setItem(mockStorageApi, prefix + "1", timeline1);
var storedRoom = store.getRoom(roomId);
expect(storedRoom).not.toBeNull();
// should only get up to the batch num timeline events
expect(storedRoom.timeline.length).toEqual(batchNum);
var timeline = timeline0.concat(timeline1);
for (i = 0; i < batchNum; i++) {
expect(storedRoom.timeline[batchNum - 1 - i].event).toEqual(
timeline[timeline.length - 1 - i]
);
}
});
it("should sync the timeline for 'live' events " +
"(full hi batch; 1+bit live batches)", function() {
// 1 and a bit events go into _live
var timelineLive = [];
timelineLive.push(utils.mkMessage({user: userId, room: roomId}));
for (i = 0; i < batchNum; i++) {
timelineLive.push(
utils.mkMessage({user: userId, room: roomId})
);
}
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", timeline0);
setItem(mockStorageApi, prefix + "1", timeline1);
setItem(mockStorageApi,
// deep copy the timeline via parse/stringify else items will
// be shift()ed from timelineLive and we can't compare!
prefix + "live", JSON.parse(JSON.stringify(timelineLive))
);
var storedRoom = store.getRoom(roomId);
expect(storedRoom).not.toBeNull();
// should only get up to the batch num timeline events (highest
// index of timelineLive is the newest message)
expect(storedRoom.timeline.length).toEqual(batchNum);
for (i = 0; i < batchNum; i++) {
expect(storedRoom.timeline[i].event).toEqual(
timelineLive[i + 1]
);
}
});
it("should sync the timeline for 'live' events " +
"(no low batch; 1 live batches)", function() {
var timelineLive = [];
for (i = 0; i < batchNum; i++) {
timelineLive.push(
utils.mkMessage({user: userId, room: roomId})
);
}
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", []);
setItem(mockStorageApi,
// deep copy the timeline via parse/stringify else items will
// be shift()ed from timelineLive and we can't compare!
prefix + "live", JSON.parse(JSON.stringify(timelineLive))
);
var storedRoom = store.getRoom(roomId);
expect(storedRoom).not.toBeNull();
// should only get up to the batch num timeline events (highest
// index of timelineLive is the newest message)
expect(storedRoom.timeline.length).toEqual(batchNum);
for (i = 0; i < batchNum; i++) {
expect(storedRoom.timeline[i].event).toEqual(
timelineLive[i]
);
}
});
it("should be able to reconstruct the timeline with negative indices",
function() {
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "-5", timeline0);
setItem(mockStorageApi, prefix + "-4", timeline1);
var timeline = timeline0.concat(timeline1);
var storedRoom = store.getRoom(roomId);
expect(storedRoom).not.toBeNull();
// should only get up to the batch num timeline events
expect(storedRoom.timeline.length).toEqual(batchNum);
for (i = 0; i < batchNum; i++) {
expect(storedRoom.timeline[batchNum - 1 - i].event).toEqual(
timeline[timeline.length - 1 - i]
);
}
});
it("should return null if the room doesn't exist", function() {
expect(store.getRoom("nothing")).toEqual(null);
});
it("should assign a storageToken to the Room", function() {
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", timeline0);
setItem(mockStorageApi, prefix + "1", timeline1);
var storedRoom = store.getRoom(roomId);
expect(storedRoom.storageToken).toBeDefined();
});
});
describe("scrollback", function() {
// stored timeline events
var timeline0, timeline1, timeline2;
beforeEach(function() {
// batch size is 3
store = new WebStorageStore(mockStorageApi, 3);
timeline0 = [
// _
utils.mkMessage({user: userId, room: roomId}), // 1 OLDEST
utils.mkMessage({user: userId, room: roomId}) // 2
];
timeline1 = [
utils.mkMessage({user: userId, room: roomId}), // 3
utils.mkMessage({user: userId, room: roomId}), // 4
utils.mkMessage({user: userId, room: roomId}) // 5
];
timeline2 = [
utils.mkMessage({user: userId, room: roomId}), // 6
utils.mkMessage({user: userId, room: roomId}), // 7
utils.mkMessage({user: userId, room: roomId}) // 8 NEWEST
];
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", timeline0);
setItem(mockStorageApi, prefix + "1", timeline1);
setItem(mockStorageApi, prefix + "2", timeline2);
});
it("should scroll back locally giving 'limit' events", function() {
var storedRoom = store.getRoom(roomId);
expect(storedRoom.timeline.length).toEqual(3);
var events = store.scrollback(storedRoom, 3);
expect(events.length).toEqual(3);
expect(events.reverse()).toEqual(timeline1);
});
it("should give less than 'limit' events near the end of the stored timeline",
function() {
var storedRoom = store.getRoom(roomId);
expect(storedRoom.timeline.length).toEqual(3);
var events = store.scrollback(storedRoom, 7);
expect(events.length).toEqual(5);
expect(events.reverse()).toEqual(timeline0.concat(timeline1));
});
it("should progressively give older messages the more times scrollback is called",
function() {
var events;
var storedRoom = store.getRoom(roomId);
expect(storedRoom.timeline.length).toEqual(3);
events = store.scrollback(storedRoom, 2);
expect(events.reverse()).toEqual([timeline1[1], timeline1[2]]);
expect(storedRoom.timeline.length).toEqual(5);
events = store.scrollback(storedRoom, 2);
expect(events.reverse()).toEqual([timeline0[1], timeline1[0]]);
expect(storedRoom.timeline.length).toEqual(7);
events = store.scrollback(storedRoom, 2);
expect(events).toEqual([timeline0[0]]);
expect(storedRoom.timeline.length).toEqual(8);
events = store.scrollback(storedRoom, 2);
expect(events).toEqual([]);
expect(storedRoom.timeline.length).toEqual(8);
});
it("should give 0 events if there is no token on the room", function() {
var r = new Room(roomId);
expect(store.scrollback(r, 3)).toEqual([]);
});
it("should give 0 events for unknown rooms", function() {
var r = new Room("!unknown:room");
r.storageToken = "foo";
expect(store.scrollback(r, 3)).toEqual([]);
});
it("should give 0 events if the boundary event is the last in the timeline",
function() {
var events;
var storedRoom = store.getRoom(roomId);
expect(storedRoom.timeline.length).toEqual(3);
// go up to the boundary (8 messages total)
events = store.scrollback(storedRoom, 5);
expect(events.length).toEqual(5);
events = store.scrollback(storedRoom, 5);
expect(events.length).toEqual(0);
});
});
describe("storeEvents", function() {
var timeline0, i;
beforeEach(function() {
timeline0 = [];
for (i = 0; i < batchNum; i++) {
timeline0.push(utils.mkMessage({user: userId, room: roomId}));
}
setItem(mockStorageApi, stateKeyName, {
events: stateEventMap,
pagination_token: "tok"
});
setItem(mockStorageApi, prefix + "0", timeline0);
});
it("should add to the live batch", function() {
var events = [
utils.mkMessage({user: userId, room: roomId, event: true}),
utils.mkMessage({user: userId, room: roomId, event: true})
];
store.storeEvents(room, events, "atoken");
var liveEvents = getItem(mockStorageApi, prefix + "live");
expect(liveEvents.length).toEqual(2);
expect(liveEvents[0]).toEqual(events[0].event);
expect(liveEvents[1]).toEqual(events[1].event);
});
it("should preserve existing live events in the store", function() {
var existingEvent = utils.mkMessage({user: userId, room: roomId});
setItem(mockStorageApi, prefix + "live", [existingEvent]);
var events = [
utils.mkMessage({user: userId, room: roomId, event: true}),
utils.mkMessage({user: userId, room: roomId, event: true})
];
store.storeEvents(room, events, "atoken");
var liveEvents = getItem(mockStorageApi, prefix + "live");
expect(liveEvents.length).toEqual(3);
expect(liveEvents[0]).toEqual(existingEvent);
expect(liveEvents[1]).toEqual(events[0].event);
expect(liveEvents[2]).toEqual(events[1].event);
});
it("should add to the lowest batch index if toStart=true", function() {
var events = [
utils.mkMessage({user: userId, room: roomId, event: true}),
utils.mkMessage({user: userId, room: roomId, event: true})
];
store.storeEvents(room, events, "atoken", true);
var timelineNeg1 = getItem(mockStorageApi, prefix + "-1");
expect(timelineNeg1.length).toEqual(2);
expect(timelineNeg1[0]).toEqual(events[1].event);
expect(timelineNeg1[1]).toEqual(events[0].event);
});
it("should add multiple batches to the lowest batch index if toStart=true",
function() {
var timelineNeg1 = [];
var timelineNeg2 = [];
for (i = 0; i < batchNum; i++) {
timelineNeg1.push(
utils.mkMessage({user: userId, room: roomId, event: true})
);
timelineNeg2.push(
utils.mkMessage({user: userId, room: roomId, event: true})
);
}
var events = timelineNeg2.concat(timelineNeg1).reverse();
store.storeEvents(room, events, "atoken", true);
var storedNeg1 = getItem(mockStorageApi, prefix + "-1");
var storedNeg2 = getItem(mockStorageApi, prefix + "-2");
expect(timelineNeg1.length).toEqual(storedNeg1.length);
expect(timelineNeg2.length).toEqual(storedNeg2.length);
for (i = 0; i < timelineNeg1.length; i++) {
expect(timelineNeg1[i].event).toEqual(storedNeg1[i]);
expect(timelineNeg2[i].event).toEqual(storedNeg2[i]);
}
});
it("should update stored state if state events exist", function() {
var events = [
utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", event: true,
content: {
name: "Room Name Here for updates"
}
})
];
room.currentState.setStateEvents(events);
store.storeEvents(room, events, "atoken");
var liveEvents = getItem(mockStorageApi, prefix + "live");
expect(liveEvents.length).toEqual(1);
expect(liveEvents[0]).toEqual(events[0].event);
var stateEvents = getItem(mockStorageApi, stateKeyName);
expect(stateEvents.events["m.room.name"][""]).toEqual(events[0].event);
});
});
describe("getRooms", function() {
var mkState = function(id) {
return [
utils.mkEvent({
event: true, type: "m.room.create", user: userId, room: id,
content: {
creator: userId
}
}),
utils.mkMembership({
event: true, user: userId, room: id, mship: "join"
})
];
};
it("should get all rooms in the store", function() {
var roomIds = [
"!alpha:bet", "!beta:fet"
];
// store 2 dynamically
var roomA = new Room(roomIds[0]);
roomA.currentState.setStateEvents(mkState(roomIds[0]));
var roomB = new Room(roomIds[1]);
roomB.currentState.setStateEvents(mkState(roomIds[1]));
store.storeRoom(roomA);
store.storeRoom(roomB);
var rooms = store.getRooms();
expect(rooms.length).toEqual(2);
for (var i = 0; i < rooms.length; i++) {
var index = roomIds.indexOf(rooms[i].roomId);
expect(index).not.toEqual(
-1, "Unknown room"
);
roomIds.splice(index, 1);
}
});
});
describe("getUser", function() {
it("should be able to retrieve a stored user", function() {
var user = new User(userId);
store.storeUser(user);
var result = store.getUser(userId);
expect(result).toBeDefined();
expect(result.userId).toEqual(userId);
});
it("should be able to retrieve a stored user with name data", function() {
var presence = utils.mkEvent({
type: "m.presence", event: true, content: {
user_id: userId,
displayname: "Flibble"
}
});
var user = new User(userId);
user.setPresenceEvent(presence);
store.storeUser(user);
var result = store.getUser(userId);
console.log(result);
expect(result.events.presence).toEqual(presence);
});
});
});
function getItem(store, key) {
return JSON.parse(store.getItem(key));
}
function setItem(store, key, val) {
store.setItem(key, JSON.stringify(val));
}
+284 -131
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -22,8 +23,8 @@ limitations under the License.
* @module base-apis
*/
var httpApi = require("./http-api");
var utils = require("./utils");
const httpApi = require("./http-api");
const utils = require("./utils");
/**
* Low-level wrappers for the Matrix APIs
@@ -45,6 +46,10 @@ var utils = require("./utils");
*
* @param {string} opts.accessToken The access_token for this user.
*
* @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of
* time to wait before timing out HTTP requests. If not specified, there is no
* timeout.
*
* @param {Object} opts.queryParams Optional. Extra query parameters to append
* to all requests with this client. Useful for application services which require
* <code>?user_id=</code>.
@@ -56,14 +61,15 @@ function MatrixBaseApis(opts) {
this.baseUrl = opts.baseUrl;
this.idBaseUrl = opts.idBaseUrl;
var httpOpts = {
const httpOpts = {
baseUrl: opts.baseUrl,
idBaseUrl: opts.idBaseUrl,
accessToken: opts.accessToken,
request: opts.request,
prefix: httpApi.PREFIX_R0,
onlyData: true,
extraParams: opts.queryParams
extraParams: opts.queryParams,
localTimeoutMs: opts.localTimeoutMs,
};
this._http = new httpApi.MatrixHttpApi(this, httpOpts);
@@ -119,7 +125,9 @@ MatrixBaseApis.prototype.makeTxnId = function() {
* @param {string} password
* @param {string} sessionId
* @param {Object} auth
* @param {boolean} bindEmail
* @param {Object} bindThreepids Set key 'email' to true to bind any email
* threepid uses during registration in the ID server. Set 'msisdn' to
* true to bind msisdn.
* @param {string} guestAccessToken
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
@@ -127,21 +135,50 @@ MatrixBaseApis.prototype.makeTxnId = function() {
*/
MatrixBaseApis.prototype.register = function(
username, password,
sessionId, auth, bindEmail, guestAccessToken,
callback
sessionId, auth, bindThreepids, guestAccessToken,
callback,
) {
if (auth === undefined) { auth = {}; }
if (sessionId) { auth.session = sessionId; }
// backwards compat
if (bindThreepids === true) {
bindThreepids = {email: true};
} else if (bindThreepids === null || bindThreepids === undefined) {
bindThreepids = {};
}
var params = {
auth: auth
if (auth === undefined || auth === null) {
auth = {};
}
if (sessionId) {
auth.session = sessionId;
}
const params = {
auth: auth,
};
if (username !== undefined && username !== null) { params.username = username; }
if (password !== undefined && password !== null) { params.password = password; }
if (bindEmail !== undefined && bindEmail !== null) { params.bind_email = bindEmail; }
if (username !== undefined && username !== null) {
params.username = username;
}
if (password !== undefined && password !== null) {
params.password = password;
}
if (bindThreepids.email) {
params.bind_email = true;
}
if (bindThreepids.msisdn) {
params.bind_msisdn = true;
}
if (guestAccessToken !== undefined && guestAccessToken !== null) {
params.guest_access_token = guestAccessToken;
}
// Temporary parameter added to make the register endpoint advertise
// msisdn flows. This exists because there are clients that break
// when given stages they don't recognise. This parameter will cease
// to be necessary once these old clients are gone.
// Only send it if we send any params at all (the password param is
// mandatory, so if we send any params, we'll send the password param)
if (password !== undefined && password !== null) {
params.x_show_msisdn = true;
}
return this.registerRequest(params, undefined, callback);
};
@@ -168,11 +205,13 @@ MatrixBaseApis.prototype.registerGuest = function(opts, callback) {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.registerRequest = function(data, kind, callback) {
var params = {};
if (kind) { params.kind = kind; }
const params = {};
if (kind) {
params.kind = kind;
}
return this._http.request(
callback, "POST", "/register", params, data
callback, "POST", "/register", params, data,
);
};
@@ -193,7 +232,7 @@ MatrixBaseApis.prototype.loginFlows = function(callback) {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.login = function(loginType, data, callback) {
var login_data = {
const login_data = {
type: loginType,
};
@@ -201,7 +240,7 @@ MatrixBaseApis.prototype.login = function(loginType, data, callback) {
utils.extend(login_data, data);
return this._http.authedRequest(
callback, "POST", "/login", undefined, login_data
callback, "POST", "/login", undefined, login_data,
);
};
@@ -215,7 +254,7 @@ MatrixBaseApis.prototype.login = function(loginType, data, callback) {
MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback) {
return this.login("m.login.password", {
user: user,
password: password
password: password,
}, callback);
};
@@ -227,7 +266,7 @@ MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback)
*/
MatrixBaseApis.prototype.loginWithSAML2 = function(relayState, callback) {
return this.login("m.login.saml2", {
relay_state: relayState
relay_state: relayState,
}, callback);
};
@@ -238,7 +277,7 @@ MatrixBaseApis.prototype.loginWithSAML2 = function(relayState, callback) {
*/
MatrixBaseApis.prototype.getCasLoginUrl = function(redirectUrl) {
return this._http.getUrl("/login/cas/redirect", {
"redirectUrl": redirectUrl
"redirectUrl": redirectUrl,
}, httpApi.PREFIX_UNSTABLE);
};
@@ -250,7 +289,7 @@ MatrixBaseApis.prototype.getCasLoginUrl = function(redirectUrl) {
*/
MatrixBaseApis.prototype.loginWithToken = function(token, callback) {
return this.login("m.login.token", {
token: token
token: token,
}, callback);
};
@@ -266,7 +305,7 @@ MatrixBaseApis.prototype.loginWithToken = function(token, callback) {
*/
MatrixBaseApis.prototype.logout = function(callback) {
return this._http.authedRequest(
callback, "POST", '/logout'
callback, "POST", '/logout',
);
};
@@ -281,14 +320,14 @@ MatrixBaseApis.prototype.logout = function(callback) {
* @return {module:client.Promise} Resolves: On success, the empty object
*/
MatrixBaseApis.prototype.deactivateAccount = function(auth, callback) {
var body = {};
let body = {};
if (auth) {
body = {
auth: auth,
};
}
return this._http.authedRequestWithPrefix(
callback, "POST", '/account/deactivate', undefined, body, httpApi.PREFIX_UNSTABLE
callback, "POST", '/account/deactivate', undefined, body, httpApi.PREFIX_UNSTABLE,
);
};
@@ -301,7 +340,7 @@ MatrixBaseApis.prototype.deactivateAccount = function(auth, callback) {
* @return {string} HS URL to hit to for the fallback interface
*/
MatrixBaseApis.prototype.getFallbackAuthUrl = function(loginType, authSessionId) {
var path = utils.encodeUri("/auth/$loginType/fallback/web", {
const path = utils.encodeUri("/auth/$loginType/fallback/web", {
$loginType: loginType,
});
@@ -330,7 +369,7 @@ MatrixBaseApis.prototype.getFallbackAuthUrl = function(loginType, authSessionId)
MatrixBaseApis.prototype.createRoom = function(options, callback) {
// valid options include: room_alias_name, visibility, invite
return this._http.authedRequest(
callback, "POST", "/createRoom", undefined, options
callback, "POST", "/createRoom", undefined, options,
);
};
@@ -341,7 +380,7 @@ MatrixBaseApis.prototype.createRoom = function(options, callback) {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.roomState = function(roomId, callback) {
var path = utils.encodeUri("/rooms/$roomId/state", {$roomId: roomId});
const path = utils.encodeUri("/rooms/$roomId/state", {$roomId: roomId});
return this._http.authedRequest(callback, "GET", path);
};
@@ -355,17 +394,17 @@ MatrixBaseApis.prototype.roomState = function(roomId, callback) {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getStateEvent = function(roomId, eventType, stateKey, callback) {
var pathParams = {
const pathParams = {
$roomId: roomId,
$eventType: eventType,
$stateKey: stateKey
$stateKey: stateKey,
};
var path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
if (stateKey !== undefined) {
path = utils.encodeUri(path + "/$stateKey", pathParams);
}
return this._http.authedRequest(
callback, "GET", path
callback, "GET", path,
);
};
@@ -380,17 +419,17 @@ MatrixBaseApis.prototype.getStateEvent = function(roomId, eventType, stateKey, c
*/
MatrixBaseApis.prototype.sendStateEvent = function(roomId, eventType, content, stateKey,
callback) {
var pathParams = {
const pathParams = {
$roomId: roomId,
$eventType: eventType,
$stateKey: stateKey
$stateKey: stateKey,
};
var path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
let path = utils.encodeUri("/rooms/$roomId/state/$eventType", pathParams);
if (stateKey !== undefined) {
path = utils.encodeUri(path + "/$stateKey", pathParams);
}
return this._http.authedRequest(
callback, "PUT", path, undefined, content
callback, "PUT", path, undefined, content,
);
};
@@ -402,9 +441,9 @@ MatrixBaseApis.prototype.sendStateEvent = function(roomId, eventType, content, s
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.redactEvent = function(roomId, eventId, callback) {
var path = utils.encodeUri("/rooms/$roomId/redact/$eventId", {
const path = utils.encodeUri("/rooms/$roomId/redact/$eventId", {
$roomId: roomId,
$eventId: eventId
$eventId: eventId,
});
return this._http.authedRequest(callback, "POST", path, undefined, {});
};
@@ -417,15 +456,17 @@ MatrixBaseApis.prototype.redactEvent = function(roomId, eventId, callback) {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) {
if (utils.isFunction(limit)) { callback = limit; limit = undefined; }
var path = utils.encodeUri("/rooms/$roomId/initialSync",
{$roomId: roomId}
if (utils.isFunction(limit)) {
callback = limit; limit = undefined;
}
const path = utils.encodeUri("/rooms/$roomId/initialSync",
{$roomId: roomId},
);
if (!limit) {
limit = 30;
}
return this._http.authedRequest(
callback, "GET", path, { limit: limit }
callback, "GET", path, { limit: limit },
);
};
@@ -434,6 +475,7 @@ MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) {
// =========================
/**
* @param {Object} options Options for this request
* @param {string} options.server The remote server to query for the room list.
* Optional. If unspecified, get the local home
* server's public room list.
@@ -454,7 +496,7 @@ MatrixBaseApis.prototype.publicRooms = function(options, callback) {
options = {};
}
var query_params = {};
const query_params = {};
if (options.server) {
query_params.server = options.server;
delete options.server;
@@ -464,7 +506,7 @@ MatrixBaseApis.prototype.publicRooms = function(options, callback) {
return this._http.authedRequest(callback, "GET", "/publicRooms");
} else {
return this._http.authedRequest(
callback, "POST", "/publicRooms", query_params, options
callback, "POST", "/publicRooms", query_params, options,
);
}
};
@@ -478,14 +520,14 @@ MatrixBaseApis.prototype.publicRooms = function(options, callback) {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.createAlias = function(alias, roomId, callback) {
var path = utils.encodeUri("/directory/room/$alias", {
$alias: alias
const path = utils.encodeUri("/directory/room/$alias", {
$alias: alias,
});
var data = {
room_id: roomId
const data = {
room_id: roomId,
};
return this._http.authedRequest(
callback, "PUT", path, undefined, data
callback, "PUT", path, undefined, data,
);
};
@@ -498,11 +540,11 @@ MatrixBaseApis.prototype.createAlias = function(alias, roomId, callback) {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.deleteAlias = function(alias, callback) {
var path = utils.encodeUri("/directory/room/$alias", {
$alias: alias
const path = utils.encodeUri("/directory/room/$alias", {
$alias: alias,
});
return this._http.authedRequest(
callback, "DELETE", path, undefined, undefined
callback, "DELETE", path, undefined, undefined,
);
};
@@ -515,11 +557,11 @@ MatrixBaseApis.prototype.deleteAlias = function(alias, callback) {
*/
MatrixBaseApis.prototype.getRoomIdForAlias = function(alias, callback) {
// TODO: deprecate this or resolveRoomAlias
var path = utils.encodeUri("/directory/room/$alias", {
$alias: alias
const path = utils.encodeUri("/directory/room/$alias", {
$alias: alias,
});
return this._http.authedRequest(
callback, "GET", path
callback, "GET", path,
);
};
@@ -531,7 +573,7 @@ MatrixBaseApis.prototype.getRoomIdForAlias = function(alias, callback) {
*/
MatrixBaseApis.prototype.resolveRoomAlias = function(roomAlias, callback) {
// TODO: deprecate this or getRoomIdForAlias
var path = utils.encodeUri("/directory/room/$alias", {$alias: roomAlias});
const path = utils.encodeUri("/directory/room/$alias", {$alias: roomAlias});
return this._http.request(callback, "GET", path);
};
@@ -544,8 +586,8 @@ MatrixBaseApis.prototype.resolveRoomAlias = function(roomAlias, callback) {
*/
MatrixBaseApis.prototype.getRoomDirectoryVisibility =
function(roomId, callback) {
var path = utils.encodeUri("/directory/list/room/$roomId", {
$roomId: roomId
const path = utils.encodeUri("/directory/list/room/$roomId", {
$roomId: roomId,
});
return this._http.authedRequest(callback, "GET", path);
};
@@ -562,11 +604,35 @@ MatrixBaseApis.prototype.getRoomDirectoryVisibility =
*/
MatrixBaseApis.prototype.setRoomDirectoryVisibility =
function(roomId, visibility, callback) {
var path = utils.encodeUri("/directory/list/room/$roomId", {
$roomId: roomId
const path = utils.encodeUri("/directory/list/room/$roomId", {
$roomId: roomId,
});
return this._http.authedRequest(
callback, "PUT", path, undefined, { "visibility": visibility }
callback, "PUT", path, undefined, { "visibility": visibility },
);
};
/**
* Set the visbility of a room bridged to a 3rd party network in
* the current HS's room directory.
* @param {string} networkId the network ID of the 3rd party
* instance under which this room is published under.
* @param {string} roomId
* @param {string} visibility "public" to make the room visible
* in the public directory, or "private" to make
* it invisible.
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setRoomDirectoryVisibilityAppService =
function(networkId, roomId, visibility, callback) {
const path = utils.encodeUri("/directory/list/appservice/$networkId/$roomId", {
$networkId: networkId,
$roomId: roomId,
});
return this._http.authedRequest(
callback, "PUT", path, undefined, { "visibility": visibility },
);
};
@@ -644,9 +710,11 @@ MatrixBaseApis.prototype.getCurrentUploads = function() {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getProfileInfo = function(userId, info, callback) {
if (utils.isFunction(info)) { callback = info; info = undefined; }
if (utils.isFunction(info)) {
callback = info; info = undefined;
}
var path = info ?
const path = info ?
utils.encodeUri("/profile/$userId/$info",
{ $userId: userId, $info: info }) :
utils.encodeUri("/profile/$userId",
@@ -664,9 +732,9 @@ MatrixBaseApis.prototype.getProfileInfo = function(userId, info, callback) {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getThreePids = function(callback) {
var path = "/account/3pid";
const path = "/account/3pid";
return this._http.authedRequest(
callback, "GET", path, undefined, undefined
callback, "GET", path, undefined, undefined,
);
};
@@ -678,13 +746,32 @@ MatrixBaseApis.prototype.getThreePids = function(callback) {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) {
var path = "/account/3pid";
var data = {
const path = "/account/3pid";
const data = {
'threePidCreds': creds,
'bind': bind
'bind': bind,
};
return this._http.authedRequest(
callback, "POST", path, null, data
callback, "POST", path, null, data,
);
};
/**
* @param {string} medium The threepid medium (eg. 'email')
* @param {string} address The threepid address (eg. 'bob@example.com')
* this must be as returned by getThreePids.
* @return {module:client.Promise} Resolves: The server response on success
* (generally the empty JSON object)
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.deleteThreePid = function(medium, address) {
const path = "/account/3pid/delete";
const data = {
'medium': medium,
'address': address,
};
return this._http.authedRequestWithPrefix(
undefined, "POST", path, null, data, httpApi.PREFIX_UNSTABLE,
);
};
@@ -697,14 +784,14 @@ MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setPassword = function(authDict, newPassword, callback) {
var path = "/account/password";
var data = {
const path = "/account/password";
const data = {
'auth': authDict,
'new_password': newPassword
'new_password': newPassword,
};
return this._http.authedRequest(
callback, "POST", path, null, data
callback, "POST", path, null, data,
);
};
@@ -718,10 +805,10 @@ MatrixBaseApis.prototype.setPassword = function(authDict, newPassword, callback)
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getDevices = function() {
var path = "/devices";
const path = "/devices";
return this._http.authedRequestWithPrefix(
undefined, "GET", path, undefined, undefined,
httpApi.PREFIX_UNSTABLE
httpApi.PREFIX_UNSTABLE,
);
};
@@ -734,14 +821,14 @@ MatrixBaseApis.prototype.getDevices = function() {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setDeviceDetails = function(device_id, body) {
var path = utils.encodeUri("/devices/$device_id", {
const path = utils.encodeUri("/devices/$device_id", {
$device_id: device_id,
});
return this._http.authedRequestWithPrefix(
undefined, "PUT", path, undefined, body,
httpApi.PREFIX_UNSTABLE
httpApi.PREFIX_UNSTABLE,
);
};
@@ -754,11 +841,11 @@ MatrixBaseApis.prototype.setDeviceDetails = function(device_id, body) {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.deleteDevice = function(device_id, auth) {
var path = utils.encodeUri("/devices/$device_id", {
const path = utils.encodeUri("/devices/$device_id", {
$device_id: device_id,
});
var body = {};
const body = {};
if (auth) {
body.auth = auth;
@@ -766,7 +853,7 @@ MatrixBaseApis.prototype.deleteDevice = function(device_id, auth) {
return this._http.authedRequestWithPrefix(
undefined, "DELETE", path, undefined, body,
httpApi.PREFIX_UNSTABLE
httpApi.PREFIX_UNSTABLE,
);
};
@@ -782,9 +869,9 @@ MatrixBaseApis.prototype.deleteDevice = function(device_id, auth) {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getPushers = function(callback) {
var path = "/pushers";
const path = "/pushers";
return this._http.authedRequest(
callback, "GET", path, undefined, undefined
callback, "GET", path, undefined, undefined,
);
};
@@ -797,9 +884,9 @@ MatrixBaseApis.prototype.getPushers = function(callback) {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.setPusher = function(pusher, callback) {
var path = "/pushers/set";
const path = "/pushers/set";
return this._http.authedRequest(
callback, "POST", path, null, pusher
callback, "POST", path, null, pusher,
);
};
@@ -823,12 +910,12 @@ MatrixBaseApis.prototype.getPushRules = function(callback) {
*/
MatrixBaseApis.prototype.addPushRule = function(scope, kind, ruleId, body, callback) {
// NB. Scope not uri encoded because devices need the '/'
var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
$kind: kind,
$ruleId: ruleId
$ruleId: ruleId,
});
return this._http.authedRequest(
callback, "PUT", path, undefined, body
callback, "PUT", path, undefined, body,
);
};
@@ -842,9 +929,9 @@ MatrixBaseApis.prototype.addPushRule = function(scope, kind, ruleId, body, callb
*/
MatrixBaseApis.prototype.deletePushRule = function(scope, kind, ruleId, callback) {
// NB. Scope not uri encoded because devices need the '/'
var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId", {
$kind: kind,
$ruleId: ruleId
$ruleId: ruleId,
});
return this._http.authedRequest(callback, "DELETE", path);
};
@@ -861,12 +948,12 @@ MatrixBaseApis.prototype.deletePushRule = function(scope, kind, ruleId, callback
*/
MatrixBaseApis.prototype.setPushRuleEnabled = function(scope, kind,
ruleId, enabled, callback) {
var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/enabled", {
const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/enabled", {
$kind: kind,
$ruleId: ruleId
$ruleId: ruleId,
});
return this._http.authedRequest(
callback, "PUT", path, undefined, {"enabled": enabled}
callback, "PUT", path, undefined, {"enabled": enabled},
);
};
@@ -882,12 +969,12 @@ MatrixBaseApis.prototype.setPushRuleEnabled = function(scope, kind,
*/
MatrixBaseApis.prototype.setPushRuleActions = function(scope, kind,
ruleId, actions, callback) {
var path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/actions", {
const path = utils.encodeUri("/pushrules/" + scope + "/$kind/$ruleId/actions", {
$kind: kind,
$ruleId: ruleId
$ruleId: ruleId,
});
return this._http.authedRequest(
callback, "PUT", path, undefined, {"actions": actions}
callback, "PUT", path, undefined, {"actions": actions},
);
};
@@ -905,12 +992,12 @@ MatrixBaseApis.prototype.setPushRuleActions = function(scope, kind,
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.search = function(opts, callback) {
var queryparams = {};
const queryparams = {};
if (opts.next_batch) {
queryparams.next_batch = opts.next_batch;
}
return this._http.authedRequest(
callback, "POST", "/search", queryparams, opts.body
callback, "POST", "/search", queryparams, opts.body,
);
};
@@ -934,8 +1021,8 @@ MatrixBaseApis.prototype.search = function(opts, callback) {
*/
MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) {
opts = opts || {};
var deviceId = opts.device_id;
var path;
const deviceId = opts.device_id;
let path;
if (deviceId) {
path = utils.encodeUri("/keys/upload/$deviceId", {
$deviceId: deviceId,
@@ -944,7 +1031,7 @@ MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) {
path = "/keys/upload";
}
return this._http.authedRequestWithPrefix(
callback, "POST", path, undefined, content, httpApi.PREFIX_UNSTABLE
callback, "POST", path, undefined, content, httpApi.PREFIX_UNSTABLE,
);
};
@@ -953,28 +1040,43 @@ MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) {
*
* @param {string[]} userIds list of users to get keys for
*
* @param {module:client.callback=} callback
* @param {Object=} opts
*
* @param {string=} opts.token sync token to pass in the query request, to help
* the HS give the most recent results
*
* @return {module:client.Promise} Resolves: result object. Rejects: with
* an error response ({@link module:http-api.MatrixError}).
*/
MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, callback) {
var downloadQuery = {};
for (var i = 0; i < userIds.length; ++i) {
downloadQuery[userIds[i]] = {};
MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) {
if (utils.isFunction(opts)) {
// opts used to be 'callback'.
throw new Error(
'downloadKeysForUsers no longer accepts a callback parameter',
);
}
var content = {device_keys: downloadQuery};
opts = opts || {};
const content = {
device_keys: {},
};
if ('token' in opts) {
content.token = opts.token;
}
userIds.forEach((u) => {
content.device_keys[u] = {};
});
return this._http.authedRequestWithPrefix(
callback, "POST", "/keys/query", undefined, content,
httpApi.PREFIX_UNSTABLE
undefined, "POST", "/keys/query", undefined, content,
httpApi.PREFIX_UNSTABLE,
);
};
/**
* Claim one-time keys
*
* @param {string[][]} devices a list of [userId, deviceId] pairs
* @param {string[]} devices a list of [userId, deviceId] pairs
*
* @param {string} [key_algorithm = signed_curve25519] desired key type
*
@@ -982,23 +1084,45 @@ MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, callback) {
* an error response ({@link module:http-api.MatrixError}).
*/
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
var queries = {};
const queries = {};
if (key_algorithm === undefined) {
key_algorithm = "signed_curve25519";
}
for (var i = 0; i < devices.length; ++i) {
var userId = devices[i][0];
var deviceId = devices[i][1];
var query = queries[userId] || {};
for (let i = 0; i < devices.length; ++i) {
const userId = devices[i][0];
const deviceId = devices[i][1];
const query = queries[userId] || {};
queries[userId] = query;
query[deviceId] = key_algorithm;
}
var content = {one_time_keys: queries};
const content = {one_time_keys: queries};
return this._http.authedRequestWithPrefix(
undefined, "POST", "/keys/claim", undefined, content,
httpApi.PREFIX_UNSTABLE
httpApi.PREFIX_UNSTABLE,
);
};
/**
* Ask the server for a list of users who have changed their device lists
* between a pair of sync tokens
*
* @param {string} oldToken
* @param {string} newToken
*
* @return {module:client.Promise} Resolves: result object. Rejects: with
* an error response ({@link module:http-api.MatrixError}).
*/
MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) {
const qps = {
from: oldToken,
to: newToken,
};
return this._http.authedRequestWithPrefix(
undefined, "GET", "/keys/changes", qps, undefined,
httpApi.PREFIX_UNSTABLE,
);
};
@@ -1028,15 +1152,44 @@ MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
*/
MatrixBaseApis.prototype.requestEmailToken = function(email, clientSecret,
sendAttempt, nextLink, callback) {
var params = {
const params = {
client_secret: clientSecret,
email: email,
send_attempt: sendAttempt,
next_link: nextLink
next_link: nextLink,
};
return this._http.idServerRequest(
callback, "POST", "/validate/email/requestToken",
params, httpApi.PREFIX_IDENTITY_V1
params, httpApi.PREFIX_IDENTITY_V1,
);
};
/**
* Submits an MSISDN token to the identity server
*
* This is used when submitting the code sent by SMS to a phone number.
* The ID server has an equivalent API for email but the js-sdk does
* not expose this, since email is normally validated by the user clicking
* a link rather than entering a code.
*
* @param {string} sid The sid given in the response to requestToken
* @param {string} clientSecret A secret binary string generated by the client.
* This must be the same value submitted in the requestToken call.
* @param {string} token The token, as enetered by the user.
*
* @return {module:client.Promise} Resolves: Object, currently with no parameters.
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if No ID server is set
*/
MatrixBaseApis.prototype.submitMsisdnToken = function(sid, clientSecret, token) {
const params = {
sid: sid,
client_secret: clientSecret,
token: token,
};
return this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, httpApi.PREFIX_IDENTITY_V1,
);
};
@@ -1052,13 +1205,13 @@ MatrixBaseApis.prototype.requestEmailToken = function(email, clientSecret,
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.lookupThreePid = function(medium, address, callback) {
var params = {
const params = {
medium: medium,
address: address,
};
return this._http.idServerRequest(
callback, "GET", "/lookup",
params, httpApi.PREFIX_IDENTITY_V1
params, httpApi.PREFIX_IDENTITY_V1,
);
};
@@ -1077,20 +1230,20 @@ MatrixBaseApis.prototype.lookupThreePid = function(medium, address, callback) {
* @return {module:client.Promise} Resolves to the result object
*/
MatrixBaseApis.prototype.sendToDevice = function(
eventType, contentMap, txnId
eventType, contentMap, txnId,
) {
var path = utils.encodeUri("/sendToDevice/$eventType/$txnId", {
const path = utils.encodeUri("/sendToDevice/$eventType/$txnId", {
$eventType: eventType,
$txnId: txnId ? txnId : this.makeTxnId(),
});
var body = {
const body = {
messages: contentMap,
};
return this._http.authedRequestWithPrefix(
undefined, "PUT", path, undefined, body,
httpApi.PREFIX_UNSTABLE
httpApi.PREFIX_UNSTABLE,
);
};
@@ -1105,7 +1258,7 @@ MatrixBaseApis.prototype.sendToDevice = function(
MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
return this._http.authedRequestWithPrefix(
undefined, "GET", "/thirdparty/protocols", undefined, undefined,
httpApi.PREFIX_UNSTABLE
httpApi.PREFIX_UNSTABLE,
);
};
@@ -1118,13 +1271,13 @@ MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
* @return {module:client.Promise} Resolves to the result object
*/
MatrixBaseApis.prototype.getThirdpartyLocation = function(protocol, params) {
var path = utils.encodeUri("/thirdparty/location/$protocol", {
$protocol: protocol
const path = utils.encodeUri("/thirdparty/location/$protocol", {
$protocol: protocol,
});
return this._http.authedRequestWithPrefix(
undefined, "GET", path, params, undefined,
httpApi.PREFIX_UNSTABLE
httpApi.PREFIX_UNSTABLE,
);
};
File diff suppressed because it is too large Load Diff
+17 -13
View File
@@ -16,7 +16,7 @@ limitations under the License.
/**
* @module content-repo
*/
var utils = require("./utils");
const utils = require("./utils");
/** Content Repo utility functions */
module.exports = {
@@ -46,9 +46,9 @@ module.exports = {
return '';
}
}
var serverAndMediaId = mxc.slice(6); // strips mxc://
var prefix = "/_matrix/media/v1/download/";
var params = {};
let serverAndMediaId = mxc.slice(6); // strips mxc://
let prefix = "/_matrix/media/v1/download/";
const params = {};
if (width) {
params.width = width;
@@ -65,8 +65,8 @@ module.exports = {
prefix = "/_matrix/media/v1/thumbnail/";
}
var fragmentOffset = serverAndMediaId.indexOf("#"),
fragment = "";
const fragmentOffset = serverAndMediaId.indexOf("#");
let fragment = "";
if (fragmentOffset >= 0) {
fragment = serverAndMediaId.substr(fragmentOffset);
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
@@ -88,18 +88,22 @@ module.exports = {
if (!identiconString) {
return null;
}
if (!width) { width = 96; }
if (!height) { height = 96; }
var params = {
if (!width) {
width = 96;
}
if (!height) {
height = 96;
}
const params = {
width: width,
height: height
height: height,
};
var path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
$ident: identiconString
const path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
$ident: identiconString,
});
return baseUrl + path +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params)));
}
},
};
+512
View File
@@ -0,0 +1,512 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module crypto/DeviceList
*
* Manages the list of other users' devices
*/
import q from 'q';
import DeviceInfo from './deviceinfo';
import olmlib from './olmlib';
/**
* @alias module:crypto/DeviceList
*/
export default class DeviceList {
constructor(baseApis, sessionStore, olmDevice) {
this._baseApis = baseApis;
this._sessionStore = sessionStore;
this._olmDevice = olmDevice;
// users with outdated device lists
// userId -> true
this._pendingUsersWithNewDevices = {};
// userId -> true
this._keyDownloadsInProgressByUser = {};
// deferred which is resolved when the current device query resolves.
// (null if there is no current request).
this._currentQueryDeferred = null;
// deferred which is resolved when the *next* device query resolves.
//
// Normally it is meaningless for this to be non-null when
// _currentQueryDeferred is null, but it can happen if the previous
// query has finished but the next one has not yet started (because the
// previous query failed, in which case we deliberately delay starting
// the next query to avoid tight-looping).
this._queuedQueryDeferred = null;
this.lastKnownSyncToken = null;
}
/**
* Download the keys for a list of users and stores the keys in the session
* store.
* @param {Array} userIds The users to fetch.
* @param {bool} forceDownload Always download the keys even if cached.
*
* @return {Promise} A promise which resolves to a map userId->deviceId->{@link
* module:crypto/deviceinfo|DeviceInfo}.
*/
downloadKeys(userIds, forceDownload) {
let needsRefresh = false;
let waitForCurrentQuery = false;
userIds.forEach((u) => {
if (this._pendingUsersWithNewDevices[u]) {
// we already know this user's devices are outdated
needsRefresh = true;
} else if (this._keyDownloadsInProgressByUser[u]) {
// already a download in progress - just wait for it.
// (even if forceDownload is true)
waitForCurrentQuery = true;
} else if (forceDownload) {
console.log("Invalidating device list for " + u +
" for forceDownload");
this.invalidateUserDeviceList(u);
needsRefresh = true;
} else if (!this.getStoredDevicesForUser(u)) {
console.log("Invalidating device list for " + u +
" due to empty cache");
this.invalidateUserDeviceList(u);
needsRefresh = true;
}
});
let promise;
if (needsRefresh) {
console.log("downloadKeys: waiting for next key query");
promise = this._startOrQueueDeviceQuery();
} else if(waitForCurrentQuery) {
console.log("downloadKeys: waiting for in-flight query to complete");
promise = this._currentQueryDeferred.promise;
} else {
// we're all up-to-date.
promise = q();
}
return promise.then(() => {
return this._getDevicesFromStore(userIds);
});
}
/**
* Get the stored device keys for a list of user ids
*
* @param {string[]} userIds the list of users to list keys for.
*
* @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}.
*/
_getDevicesFromStore(userIds) {
const stored = {};
const self = this;
userIds.map(function(u) {
stored[u] = {};
const devices = self.getStoredDevicesForUser(u) || [];
devices.map(function(dev) {
stored[u][dev.deviceId] = dev;
});
});
return stored;
}
/**
* Get the stored device keys for a user id
*
* @param {string} userId the user to list keys for.
*
* @return {module:crypto/deviceinfo[]|null} list of devices, or null if we haven't
* managed to get a list of devices for this user yet.
*/
getStoredDevicesForUser(userId) {
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
if (!devs) {
return null;
}
const res = [];
for (const deviceId in devs) {
if (devs.hasOwnProperty(deviceId)) {
res.push(DeviceInfo.fromStorage(devs[deviceId], deviceId));
}
}
return res;
}
/**
* Get the stored keys for a single device
*
* @param {string} userId
* @param {string} deviceId
*
* @return {module:crypto/deviceinfo?} device, or undefined
* if we don't know about this device
*/
getStoredDevice(userId, deviceId) {
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
if (!devs || !devs[deviceId]) {
return undefined;
}
return DeviceInfo.fromStorage(devs[deviceId], deviceId);
}
/**
* Find a device by curve25519 identity key
*
* @param {string} userId owner of the device
* @param {string} algorithm encryption algorithm
* @param {string} senderKey curve25519 key to match
*
* @return {module:crypto/deviceinfo?}
*/
getDeviceByIdentityKey(userId, algorithm, senderKey) {
if (
algorithm !== olmlib.OLM_ALGORITHM &&
algorithm !== olmlib.MEGOLM_ALGORITHM
) {
// we only deal in olm keys
return null;
}
const devices = this._sessionStore.getEndToEndDevicesForUser(userId);
if (!devices) {
return null;
}
for (const deviceId in devices) {
if (!devices.hasOwnProperty(deviceId)) {
continue;
}
const device = devices[deviceId];
for (const keyId in device.keys) {
if (!device.keys.hasOwnProperty(keyId)) {
continue;
}
if (keyId.indexOf("curve25519:") !== 0) {
continue;
}
const deviceKey = device.keys[keyId];
if (deviceKey == senderKey) {
return DeviceInfo.fromStorage(device, deviceId);
}
}
}
// doesn't match a known device
return null;
}
/**
* Mark the cached device list for the given user outdated.
*
* This doesn't set off an update, so that several users can be batched
* together. Call refreshOutdatedDeviceLists() for that.
*
* @param {String} userId
*/
invalidateUserDeviceList(userId) {
// sanity-check the userId. This is mostly paranoia, but if synapse
// can't parse the userId we give it as an mxid, it 500s the whole
// request and we can never update the device lists again (because
// the broken userId is always 'invalid' and always included in any
// refresh request).
// By checking it is at least a string, we can eliminate a class of
// silly errors.
if (typeof userId !== 'string') {
throw new Error('userId must be a string; was '+userId);
}
this._pendingUsersWithNewDevices[userId] = true;
}
/**
* If there is not already a device list query in progress, and we have
* users who have outdated device lists, start a query now.
*/
refreshOutdatedDeviceLists() {
if (this._currentQueryDeferred) {
// request already in progress - do nothing. (We will automatically
// make another request if there are more users with outdated
// device lists when the current request completes).
return;
}
this._startDeviceQuery();
}
/**
* If there is currently a device list query in progress, returns a promise
* which will resolve when the *next* query completes. Otherwise, starts
* a new query, and returns a promise which resolves when it completes.
*
* @return {Promise}
*/
_startOrQueueDeviceQuery() {
if (!this._currentQueryDeferred) {
this._startDeviceQuery();
if (!this._currentQueryDeferred) {
return q();
}
return this._currentQueryDeferred.promise;
}
if (!this._queuedQueryDeferred) {
this._queuedQueryDeferred = q.defer();
}
return this._queuedQueryDeferred.promise;
}
/**
* kick off a new device query
*
* Throws if there is already a query in progress.
*/
_startDeviceQuery() {
if (this._currentQueryDeferred) {
throw new Error("DeviceList._startDeviceQuery called with request active");
}
this._currentQueryDeferred = this._queuedQueryDeferred || q.defer();
this._queuedQueryDeferred = null;
const users = Object.keys(this._pendingUsersWithNewDevices);
if (users.length === 0) {
// nothing to do
this._currentQueryDeferred.resolve();
this._currentQueryDeferred = null;
// that means we're up-to-date with the lastKnownSyncToken.
const token = this.lastKnownSyncToken;
if (token !== null) {
this._sessionStore.storeEndToEndDeviceSyncToken(token);
}
return;
}
this._doKeyDownloadForUsers(users).done(() => {
users.forEach((u) => {
delete this._keyDownloadsInProgressByUser[u];
});
this._currentQueryDeferred.resolve();
this._currentQueryDeferred = null;
// flush out any more requests that were blocked up while that
// was going on.
this._startDeviceQuery();
}, (e) => {
console.error(
'Error updating device key cache for ' + users + ":", e,
);
// reinstate the pending flags on any users which failed; this will
// mean that we will do another download in the future (actually on
// the next /sync).
users.forEach((u) => {
delete this._keyDownloadsInProgressByUser[u];
this._pendingUsersWithNewDevices[u] = true;
});
this._currentQueryDeferred.reject(e);
this._currentQueryDeferred = null;
});
users.forEach((u) => {
delete this._pendingUsersWithNewDevices[u];
this._keyDownloadsInProgressByUser[u] = true;
});
}
/**
* @param {string[]} downloadUsers list of userIds
*
* @return {Promise}
*/
_doKeyDownloadForUsers(downloadUsers) {
console.log('Starting key download for ' + downloadUsers);
const token = this.lastKnownSyncToken;
const opts = {};
if (token) {
opts.token = token;
}
return this._baseApis.downloadKeysForUsers(
downloadUsers, opts,
).then((res) => {
const dk = res.device_keys || {};
// do each user in a separate promise, to avoid wedging the CPU
// (https://github.com/vector-im/riot-web/issues/3158)
//
// of course we ought to do this in a web worker or similar, but
// this serves as an easy solution for now.
let prom = q();
for (const userId of downloadUsers) {
prom = prom.delay(5).then(() => {
this._processQueryResponseForUser(userId, dk[userId]);
});
}
return prom;
}).then(() => {
if (token !== null) {
this._sessionStore.storeEndToEndDeviceSyncToken(token);
}
console.log('Completed key download for ' + downloadUsers);
});
}
_processQueryResponseForUser(userId, response) {
console.log('got keys for ' + userId + ':', response);
// map from deviceid -> deviceinfo for this user
const userStore = {};
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
if (devs) {
Object.keys(devs).forEach((deviceId) => {
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
userStore[deviceId] = d;
});
}
_updateStoredDeviceKeysForUser(
this._olmDevice, userId, userStore, response || {},
);
// update the session store
const storage = {};
Object.keys(userStore).forEach((deviceId) => {
storage[deviceId] = userStore[deviceId].toStorage();
});
this._sessionStore.storeEndToEndDevicesForUser(
userId, storage,
);
}
}
function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
userResult) {
let updated = false;
// remove any devices in the store which aren't in the response
for (const deviceId in userStore) {
if (!userStore.hasOwnProperty(deviceId)) {
continue;
}
if (!(deviceId in userResult)) {
console.log("Device " + userId + ":" + deviceId +
" has been removed");
delete userStore[deviceId];
updated = true;
}
}
for (const deviceId in userResult) {
if (!userResult.hasOwnProperty(deviceId)) {
continue;
}
const deviceResult = userResult[deviceId];
// check that the user_id and device_id in the response object are
// correct
if (deviceResult.user_id !== userId) {
console.warn("Mismatched user_id " + deviceResult.user_id +
" in keys from " + userId + ":" + deviceId);
continue;
}
if (deviceResult.device_id !== deviceId) {
console.warn("Mismatched device_id " + deviceResult.device_id +
" in keys from " + userId + ":" + deviceId);
continue;
}
if (_storeDeviceKeys(_olmDevice, userStore, deviceResult)) {
updated = true;
}
}
return updated;
}
/*
* Process a device in a /query response, and add it to the userStore
*
* returns true if a change was made, else false
*/
function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
if (!deviceResult.keys) {
// no keys?
return false;
}
const deviceId = deviceResult.device_id;
const userId = deviceResult.user_id;
const signKeyId = "ed25519:" + deviceId;
const signKey = deviceResult.keys[signKeyId];
if (!signKey) {
console.warn("Device " + userId + ":" + deviceId +
" has no ed25519 key");
return false;
}
const unsigned = deviceResult.unsigned || {};
try {
olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
} catch (e) {
console.warn("Unable to verify signature on device " +
userId + ":" + deviceId + ":" + e);
return false;
}
// DeviceInfo
let deviceStore;
if (deviceId in userStore) {
// already have this device.
deviceStore = userStore[deviceId];
if (deviceStore.getFingerprint() != signKey) {
// this should only happen if the list has been MITMed; we are
// best off sticking with the original keys.
//
// Should we warn the user about it somehow?
console.warn("Ed25519 key for device " + userId + ":" +
deviceId + " has changed");
return false;
}
} else {
userStore[deviceId] = deviceStore = new DeviceInfo(deviceId);
}
deviceStore.keys = deviceResult.keys || {};
deviceStore.algorithms = deviceResult.algorithms || [];
deviceStore.unsigned = unsigned;
return true;
}
@@ -20,9 +20,49 @@ limitations under the License.
*
* @module crypto/OlmDevice
*/
const Olm = require("olm");
if (!Olm) {
// this happens if we were loaded via browserify and the Olm module was not
// loaded.
throw new Error("Olm is not defined");
}
const utils = require("../utils");
// The maximum size of an event is 65K, and we base64 the content, so this is a
// reasonable approximation to the biggest plaintext we can encrypt.
const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4;
function checkPayloadLength(payloadString) {
if (payloadString === undefined) {
throw new Error("payloadString undefined");
}
if (payloadString.length > MAX_PLAINTEXT_LENGTH) {
// might as well fail early here rather than letting the olm library throw
// a cryptic memory allocation error.
//
// Note that even if we manage to do the encryption, the message send may fail,
// because by the time we've wrapped the ciphertext in the event object, it may
// exceed 65K. But at least we won't just fail with "abort()" in that case.
throw new Error("Message too long (" + payloadString.length + " bytes). " +
"The maximum for an encrypted message is " +
MAX_PLAINTEXT_LENGTH + " bytes.");
}
}
/**
* The type of object we use for importing and exporting megolm session data.
*
* @typedef {Object} module:crypto/OlmDevice.MegolmSessionData
* @property {String} sender_key Sender's Curve25519 device key
* @property {Object<string, string>} sender_claimed_keys Other keys the sender claims.
* @property {String} room_id Room this session is used in
* @property {String} session_id Unique id for the session
* @property {String} session_key Base64'ed key data
*/
var Olm = require("olm");
var utils = require("../utils");
/**
* Manages the olm cryptography functions. Each OlmDevice has a single
@@ -43,8 +83,8 @@ function OlmDevice(sessionStore) {
this._sessionStore = sessionStore;
this._pickleKey = "DEFAULT_KEY";
var e2eKeys;
var account = new Olm.Account();
let e2eKeys;
const account = new Olm.Account();
try {
_initialise_account(this._sessionStore, this._pickleKey, account);
e2eKeys = JSON.parse(account.identity_keys());
@@ -73,14 +113,14 @@ function OlmDevice(sessionStore) {
}
function _initialise_account(sessionStore, pickleKey, account) {
var e2eAccount = sessionStore.getEndToEndAccount();
const e2eAccount = sessionStore.getEndToEndAccount();
if (e2eAccount !== null) {
account.unpickle(pickleKey, e2eAccount);
return;
}
account.create();
var pickled = account.pickle(pickleKey);
const pickled = account.pickle(pickleKey);
sessionStore.storeEndToEndAccount(pickled);
}
@@ -100,9 +140,9 @@ OlmDevice.getOlmVersion = function() {
* @private
*/
OlmDevice.prototype._getAccount = function(func) {
var account = new Olm.Account();
const account = new Olm.Account();
try {
var pickledAccount = this._sessionStore.getEndToEndAccount();
const pickledAccount = this._sessionStore.getEndToEndAccount();
account.unpickle(this._pickleKey, pickledAccount);
return func(account);
} finally {
@@ -118,7 +158,7 @@ OlmDevice.prototype._getAccount = function(func) {
* @private
*/
OlmDevice.prototype._saveAccount = function(account) {
var pickledAccount = account.pickle(this._pickleKey);
const pickledAccount = account.pickle(this._pickleKey);
this._sessionStore.storeEndToEndAccount(pickledAccount);
};
@@ -133,10 +173,10 @@ OlmDevice.prototype._saveAccount = function(account) {
* @private
*/
OlmDevice.prototype._getSession = function(deviceKey, sessionId, func) {
var sessions = this._sessionStore.getEndToEndSessions(deviceKey);
var pickledSession = sessions[sessionId];
const sessions = this._sessionStore.getEndToEndSessions(deviceKey);
const pickledSession = sessions[sessionId];
var session = new Olm.Session();
const session = new Olm.Session();
try {
session.unpickle(this._pickleKey, pickledSession);
return func(session);
@@ -154,9 +194,9 @@ OlmDevice.prototype._getSession = function(deviceKey, sessionId, func) {
* @private
*/
OlmDevice.prototype._saveSession = function(deviceKey, session) {
var pickledSession = session.pickle(this._pickleKey);
const pickledSession = session.pickle(this._pickleKey);
this._sessionStore.storeEndToEndSession(
deviceKey, session.session_id(), pickledSession
deviceKey, session.session_id(), pickledSession,
);
};
@@ -169,7 +209,7 @@ OlmDevice.prototype._saveSession = function(deviceKey, session) {
* @private
*/
OlmDevice.prototype._getUtility = function(func) {
var utility = new Olm.Utility();
const utility = new Olm.Utility();
try {
return func(utility);
} finally {
@@ -219,7 +259,7 @@ OlmDevice.prototype.maxNumberOfOneTimeKeys = function() {
* Marks all of the one-time keys as published.
*/
OlmDevice.prototype.markKeysAsPublished = function() {
var self = this;
const self = this;
this._getAccount(function(account) {
account.mark_keys_as_published();
self._saveAccount(account);
@@ -232,7 +272,7 @@ OlmDevice.prototype.markKeysAsPublished = function() {
* @param {number} numKeys number of keys to generate
*/
OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
var self = this;
const self = this;
this._getAccount(function(account) {
account.generate_one_time_keys(numKeys);
self._saveAccount(account);
@@ -249,11 +289,11 @@ OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
* @return {string} sessionId for the outbound session.
*/
OlmDevice.prototype.createOutboundSession = function(
theirIdentityKey, theirOneTimeKey
theirIdentityKey, theirOneTimeKey,
) {
var self = this;
const self = this;
return this._getAccount(function(account) {
var session = new Olm.Session();
const session = new Olm.Session();
try {
session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
self._saveSession(theirIdentityKey, session);
@@ -279,21 +319,21 @@ OlmDevice.prototype.createOutboundSession = function(
* didn't use a valid one-time key).
*/
OlmDevice.prototype.createInboundSession = function(
theirDeviceIdentityKey, message_type, ciphertext
theirDeviceIdentityKey, message_type, ciphertext,
) {
if (message_type !== 0) {
throw new Error("Need message_type == 0 to create inbound session");
}
var self = this;
const self = this;
return this._getAccount(function(account) {
var session = new Olm.Session();
const 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);
const payloadString = session.decrypt(message_type, ciphertext);
self._saveSession(theirDeviceIdentityKey, session);
@@ -316,8 +356,8 @@ OlmDevice.prototype.createInboundSession = function(
* @return {string[]} a list of known session ids for the device
*/
OlmDevice.prototype.getSessionIdsForDevice = function(theirDeviceIdentityKey) {
var sessions = this._sessionStore.getEndToEndSessions(
theirDeviceIdentityKey
const sessions = this._sessionStore.getEndToEndSessions(
theirDeviceIdentityKey,
);
return utils.keys(sessions);
};
@@ -330,7 +370,7 @@ OlmDevice.prototype.getSessionIdsForDevice = function(theirDeviceIdentityKey) {
* @return {string?} session id, or null if no established session
*/
OlmDevice.prototype.getSessionIdForDevice = function(theirDeviceIdentityKey) {
var sessionIds = this.getSessionIdsForDevice(theirDeviceIdentityKey);
const sessionIds = this.getSessionIdsForDevice(theirDeviceIdentityKey);
if (sessionIds.length === 0) {
return null;
}
@@ -351,20 +391,20 @@ OlmDevice.prototype.getSessionIdForDevice = function(theirDeviceIdentityKey) {
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
*/
OlmDevice.prototype.getSessionInfoForDevice = function(deviceIdentityKey) {
var sessionIds = this.getSessionIdsForDevice(deviceIdentityKey);
const sessionIds = this.getSessionIdsForDevice(deviceIdentityKey);
sessionIds.sort();
var info = [];
const info = [];
function getSessionInfo(session) {
return {
hasReceivedMessage: session.has_received_message()
hasReceivedMessage: session.has_received_message(),
};
}
for (var i = 0; i < sessionIds.length; i++) {
var sessionId = sessionIds[i];
var res = this._getSession(deviceIdentityKey, sessionId, getSessionInfo);
for (let i = 0; i < sessionIds.length; i++) {
const sessionId = sessionIds[i];
const res = this._getSession(deviceIdentityKey, sessionId, getSessionInfo);
res.sessionId = sessionId;
info.push(res);
}
@@ -382,16 +422,14 @@ OlmDevice.prototype.getSessionInfoForDevice = function(deviceIdentityKey) {
* @return {string} ciphertext
*/
OlmDevice.prototype.encryptMessage = function(
theirDeviceIdentityKey, sessionId, payloadString
theirDeviceIdentityKey, sessionId, payloadString,
) {
var self = this;
const self = this;
if (payloadString === undefined) {
throw new Error("payloadString undefined");
}
checkPayloadLength(payloadString);
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
var res = session.encrypt(payloadString);
const res = session.encrypt(payloadString);
self._saveSession(theirDeviceIdentityKey, session);
return res;
});
@@ -409,12 +447,12 @@ OlmDevice.prototype.encryptMessage = function(
* @return {string} decrypted payload.
*/
OlmDevice.prototype.decryptMessage = function(
theirDeviceIdentityKey, sessionId, message_type, ciphertext
theirDeviceIdentityKey, sessionId, message_type, ciphertext,
) {
var self = this;
const self = this;
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
var payloadString = session.decrypt(message_type, ciphertext);
const payloadString = session.decrypt(message_type, ciphertext);
self._saveSession(theirDeviceIdentityKey, session);
return payloadString;
@@ -434,7 +472,7 @@ OlmDevice.prototype.decryptMessage = function(
* the given session.
*/
OlmDevice.prototype.matchesSession = function(
theirDeviceIdentityKey, sessionId, message_type, ciphertext
theirDeviceIdentityKey, sessionId, message_type, ciphertext,
) {
if (message_type !== 0) {
return false;
@@ -446,7 +484,6 @@ OlmDevice.prototype.matchesSession = function(
};
// Outbound group session
// ======================
@@ -457,7 +494,7 @@ OlmDevice.prototype.matchesSession = function(
* @private
*/
OlmDevice.prototype._saveOutboundGroupSession = function(session) {
var pickledSession = session.pickle(this._pickleKey);
const pickledSession = session.pickle(this._pickleKey);
this._outboundGroupSessionStore[session.session_id()] = pickledSession;
};
@@ -472,12 +509,12 @@ OlmDevice.prototype._saveOutboundGroupSession = function(session) {
* @private
*/
OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
var pickled = this._outboundGroupSessionStore[sessionId];
const pickled = this._outboundGroupSessionStore[sessionId];
if (pickled === null) {
throw new Error("Unknown outbound group session " + sessionId);
}
var session = new Olm.OutboundGroupSession();
const session = new Olm.OutboundGroupSession();
try {
session.unpickle(this._pickleKey, pickled);
return func(session);
@@ -493,7 +530,7 @@ OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
* @return {string} sessionId for the outbound session.
*/
OlmDevice.prototype.createOutboundGroupSession = function() {
var session = new Olm.OutboundGroupSession();
const session = new Olm.OutboundGroupSession();
try {
session.create();
this._saveOutboundGroupSession(session);
@@ -513,10 +550,12 @@ OlmDevice.prototype.createOutboundGroupSession = function() {
* @return {string} ciphertext
*/
OlmDevice.prototype.encryptGroupMessage = function(sessionId, payloadString) {
var self = this;
const self = this;
checkPayloadLength(payloadString);
return this._getOutboundGroupSession(sessionId, function(session) {
var res = session.encrypt(payloadString);
const res = session.encrypt(payloadString);
self._saveOutboundGroupSession(session);
return res;
});
@@ -554,16 +593,16 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
* @private
*/
OlmDevice.prototype._saveInboundGroupSession = function(
roomId, senderCurve25519Key, sessionId, session, keysClaimed
roomId, senderCurve25519Key, sessionId, session, keysClaimed,
) {
var r = {
const r = {
room_id: roomId,
session: session.pickle(this._pickleKey),
keysClaimed: keysClaimed,
};
this._sessionStore.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, JSON.stringify(r)
senderCurve25519Key, sessionId, JSON.stringify(r),
);
};
@@ -584,10 +623,10 @@ OlmDevice.prototype._saveInboundGroupSession = function(
* @template {T}
*/
OlmDevice.prototype._getInboundGroupSession = function(
roomId, senderKey, sessionId, func
roomId, senderKey, sessionId, func,
) {
var r = this._sessionStore.getEndToEndInboundGroupSession(
senderKey, sessionId
let r = this._sessionStore.getEndToEndInboundGroupSession(
senderKey, sessionId,
);
if (r === null) {
@@ -601,11 +640,11 @@ OlmDevice.prototype._getInboundGroupSession = function(
if (roomId !== r.room_id) {
throw new Error(
"Mismatched room_id for inbound group session (expected " + r.room_id +
", was " + roomId + ")"
", was " + roomId + ")",
);
}
var session = new Olm.InboundGroupSession();
const session = new Olm.InboundGroupSession();
try {
session.unpickle(this._pickleKey, r.session);
return func(session, r.keysClaimed || {});
@@ -624,9 +663,9 @@ OlmDevice.prototype._getInboundGroupSession = function(
* @param {Object<string, string>} keysClaimed Other keys the sender claims.
*/
OlmDevice.prototype.addInboundGroupSession = function(
roomId, senderKey, sessionId, sessionKey, keysClaimed
roomId, senderKey, sessionId, sessionKey, keysClaimed,
) {
var self = this;
const self = this;
/* if we already have this session, consider updating it */
function updateSession(session) {
@@ -636,8 +675,8 @@ OlmDevice.prototype.addInboundGroupSession = function(
return true;
}
var r = this._getInboundGroupSession(
roomId, senderKey, sessionId, updateSession
const r = this._getInboundGroupSession(
roomId, senderKey, sessionId, updateSession,
);
if (r !== null) {
@@ -645,16 +684,58 @@ OlmDevice.prototype.addInboundGroupSession = function(
}
// new session.
var session = new Olm.InboundGroupSession();
const session = new Olm.InboundGroupSession();
try {
session.create(sessionKey);
if (sessionId != session.session_id()) {
throw new Error(
"Mismatched group session ID from senderKey: " + senderKey
"Mismatched group session ID from senderKey: " + senderKey,
);
}
self._saveInboundGroupSession(
roomId, senderKey, sessionId, session, keysClaimed
roomId, senderKey, sessionId, session, keysClaimed,
);
} finally {
session.free();
}
};
/**
* Add a previously-exported inbound group session to the session store
*
* @param {module:crypto/OlmDevice.MegolmSessionData} data session data
*/
OlmDevice.prototype.importInboundGroupSession = function(data) {
/* if we already have this session, consider updating it */
function updateSession(session) {
console.log("Update for megolm session " + data.sender_key + "|" +
data.session_id);
// for now we just ignore updates. TODO: implement something here
return true;
}
const r = this._getInboundGroupSession(
data.room_id, data.sender_key, data.session_id, updateSession,
);
if (r !== null) {
return;
}
// new session.
const session = new Olm.InboundGroupSession();
try {
session.import_session(data.session_key);
if (data.session_id != session.session_id()) {
throw new Error(
"Mismatched group session ID from senderKey: " + data.sender_key,
);
}
this._saveInboundGroupSession(
data.room_id, data.sender_key, data.session_id, session,
data.sender_claimed_keys,
);
} finally {
session.free();
@@ -675,24 +756,24 @@ OlmDevice.prototype.addInboundGroupSession = function(
* Object<string, string>}} result
*/
OlmDevice.prototype.decryptGroupMessage = function(
roomId, senderKey, sessionId, body
roomId, senderKey, sessionId, body,
) {
var self = this;
const self = this;
function decrypt(session, keysClaimed) {
var res = session.decrypt(body);
const res = session.decrypt(body);
var plaintext = res.plaintext;
let plaintext = res.plaintext;
if (plaintext === undefined) {
// Compatibility for older olm versions.
plaintext = res;
} else {
// Check if we have seen this message index before to detect replay attacks.
var messageIndexKey = senderKey + "|" + sessionId + "|" + res.message_index;
const messageIndexKey = senderKey + "|" + sessionId + "|" + res.message_index;
if (messageIndexKey in self._inboundGroupSessionMessageIndexes) {
throw new Error(
"Duplicate message index, possible replay attack: " +
messageIndexKey
messageIndexKey,
);
}
self._inboundGroupSessionMessageIndexes[messageIndexKey] = true;
@@ -700,10 +781,10 @@ OlmDevice.prototype.decryptGroupMessage = function(
// the sender must have had the senderKey to persuade us to save the
// session.
var keysProved = {curve25519: senderKey};
const keysProved = {curve25519: senderKey};
self._saveInboundGroupSession(
roomId, senderKey, sessionId, session, keysClaimed
roomId, senderKey, sessionId, session, keysClaimed,
);
return {
result: plaintext,
@@ -713,10 +794,45 @@ OlmDevice.prototype.decryptGroupMessage = function(
}
return this._getInboundGroupSession(
roomId, senderKey, sessionId, decrypt
roomId, senderKey, sessionId, decrypt,
);
};
/**
* Export an inbound group session
*
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @return {module:crypto/OlmDevice.MegolmSessionData} exported session data
*/
OlmDevice.prototype.exportInboundGroupSession = function(senderKey, sessionId) {
const s = this._sessionStore.getEndToEndInboundGroupSession(
senderKey, sessionId,
);
if (s === null) {
throw new Error("Unknown inbound group session [" + senderKey + "," +
sessionId + "]");
}
const r = JSON.parse(s);
const session = new Olm.InboundGroupSession();
try {
session.unpickle(this._pickleKey, r.session);
const messageIndex = session.first_known_index();
return {
"sender_key": senderKey,
"sender_claimed_keys": r.keysClaimed,
"room_id": r.room_id,
"session_id": sessionId,
"session_key": session.export_session(messageIndex),
};
} finally {
session.free();
}
};
// Utilities
// =========
@@ -733,7 +849,7 @@ OlmDevice.prototype.decryptGroupMessage = function(
* was invalid then the message will be "OLM.BAD_MESSAGE_MAC".
*/
OlmDevice.prototype.verifySignature = function(
key, message, signature
key, message, signature,
) {
this._getUtility(function(util) {
util.ed25519_verify(key, message, signature);
@@ -20,7 +20,7 @@ limitations under the License.
*
* @module crypto/algorithms/base
*/
var utils = require("../../utils");
const utils = require("../../utils");
/**
* map of registered encryption algorithm classes. A map from string to {@link
@@ -53,7 +53,7 @@ module.exports.DECRYPTION_CLASSES = {};
* @param {string} params.roomId The ID of the room we will be sending to
* @param {object} params.config The body of the m.room.encryption event
*/
var EncryptionAlgorithm = function(params) {
const EncryptionAlgorithm = function(params) {
this._userId = params.userId;
this._deviceId = params.deviceId;
this._crypto = params.crypto;
@@ -85,7 +85,7 @@ module.exports.EncryptionAlgorithm = EncryptionAlgorithm;
* @param {string=} oldMembership previous membership
*/
EncryptionAlgorithm.prototype.onRoomMembership = function(
event, member, oldMembership
event, member, oldMembership,
) {};
/**
@@ -101,7 +101,7 @@ EncryptionAlgorithm.prototype.onRoomMembership = function(
* @param {string=} params.roomId The ID of the room we will be receiving
* from. Null for to-device events.
*/
var DecryptionAlgorithm = function(params) {
const DecryptionAlgorithm = function(params) {
this._userId = params.userId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
@@ -130,23 +130,77 @@ module.exports.DecryptionAlgorithm = DecryptionAlgorithm;
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent
*
* @param {module:models/event.MatrixEvent} event key event
* @param {module:models/event.MatrixEvent} params event key event
*/
DecryptionAlgorithm.prototype.onRoomKeyEvent = function(params) {
// ignore by default
};
/**
* Import a room key
*
* @param {module:crypto/OlmDevice.MegolmSessionData} session
*/
DecryptionAlgorithm.prototype.importRoomKey = function(session) {
// ignore by default
};
/**
* Exception thrown when decryption fails
*
* @alias module:crypto/algorithms/base.DecryptionError
* @constructor
* @param {string} msg user-visible message describing the problem
*
* @param {Object=} details key/value pairs reported in the logs but not shown
* to the user.
*
* @extends Error
*/
const DecryptionError = function(msg, details) {
this.name = 'DecryptionError';
this.message = msg;
this.details = details;
};
utils.inherits(DecryptionError, Error);
/** override the string used when logging
*
* @returns {String}
*/
DecryptionError.prototype.toString = function() {
let result = this.name + '[msg: ' + this.message;
if (this.details) {
result += ', ' +
Object.keys(this.details).map(
(k) => k + ': ' + this.details[k],
).join(', ');
}
result += ']';
return result;
};
module.exports.DecryptionError = DecryptionError;
/**
* Exception thrown specifically when we want to warn the user to consider
* the security of their conversation before continuing
*
* @constructor
* @param {string} msg message describing the problem
* @param {Object} devices userId -> {deviceId -> object}
* set of unknown devices per user we're warning about
* @extends Error
*/
module.exports.DecryptionError = function(msg) {
module.exports.UnknownDeviceError = function(msg, devices) {
this.name = "UnknownDeviceError";
this.message = msg;
this.devices = devices;
};
utils.inherits(module.exports.DecryptionError, Error);
utils.inherits(module.exports.UnknownDeviceError, Error);
/**
* Registers an encryption/decryption class for a particular algorithm
@@ -19,7 +19,7 @@ limitations under the License.
* @module crypto/algorithms
*/
var base = require("./base");
const base = require("./base");
require("./olm");
require("./megolm");
@@ -21,11 +21,11 @@ limitations under the License.
* @module crypto/algorithms/megolm
*/
var q = require("q");
const q = require("q");
var utils = require("../../utils");
var olmlib = require("../olmlib");
var base = require("./base");
const utils = require("../../utils");
const olmlib = require("../olmlib");
const base = require("./base");
/**
* @private
@@ -36,8 +36,6 @@ var base = require("./base");
* @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
@@ -47,7 +45,6 @@ function OutboundSessionInfo(sessionId) {
this.sessionId = sessionId;
this.useCount = 0;
this.creationTime = new Date().getTime();
this.sharePromise = null;
this.sharedWithDevices = {};
}
@@ -60,16 +57,16 @@ function OutboundSessionInfo(sessionId) {
* @return {Boolean}
*/
OutboundSessionInfo.prototype.needsRotation = function(
rotationPeriodMsgs, rotationPeriodMs
rotationPeriodMsgs, rotationPeriodMs,
) {
var sessionLifetime = new Date().getTime() - this.creationTime;
const sessionLifetime = new Date().getTime() - this.creationTime;
if (this.useCount >= rotationPeriodMsgs ||
sessionLifetime >= rotationPeriodMs
) {
console.log(
"Rotating megolm session after " + this.useCount +
" messages, " + sessionLifetime + "ms"
" messages, " + sessionLifetime + "ms",
);
return true;
}
@@ -78,6 +75,46 @@ OutboundSessionInfo.prototype.needsRotation = function(
};
/**
* Determine if this session has been shared with devices which it shouldn't
* have been.
*
* @param {Object} devicesInRoom userId -> {deviceId -> object}
* devices we should shared the session with.
*
* @return {Boolean} true if we have shared the session with devices which aren't
* in devicesInRoom.
*/
OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
devicesInRoom,
) {
for (const userId in this.sharedWithDevices) {
if (!this.sharedWithDevices.hasOwnProperty(userId)) {
continue;
}
if (!devicesInRoom.hasOwnProperty(userId)) {
console.log("Starting new session because we shared with " + userId);
return true;
}
for (const deviceId in this.sharedWithDevices[userId]) {
if (!this.sharedWithDevices[userId].hasOwnProperty(deviceId)) {
continue;
}
if (!devicesInRoom[userId].hasOwnProperty(deviceId)) {
console.log(
"Starting new session because we shared with " +
userId + ":" + deviceId,
);
return true;
}
}
}
};
/**
* Megolm encryption implementation
*
@@ -90,10 +127,12 @@ OutboundSessionInfo.prototype.needsRotation = function(
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;
// the most recent attempt to set up a session. This is used to serialise
// the session setups, so that we have a race-free view of which session we
// are using, and which devices we have shared the keys with. It resolves
// with an OutboundSessionInfo (or undefined, for the first message in the
// room).
this._setupPromise = q();
// default rotation periods
this._sessionRotationPeriodMsgs = 100;
@@ -112,51 +151,59 @@ utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
/**
* @private
*
* @param {module:models/room} room
* @param {Object} devicesInRoom The devices in this room, indexed by user ID
*
* @return {module:client.Promise} Promise which resolves to the
* OutboundSessionInfo when setup is complete.
*/
MegolmEncryption.prototype._ensureOutboundSession = function(room) {
var self = this;
MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
const self = this;
var session = this._outboundSession;
let session;
// need to make a brand new session?
if (!session || session.needsRotation(self._sessionRotationPeriodMsgs,
self._sessionRotationPeriodMs)
) {
this._outboundSession = session = this._prepareNewSession(room);
}
// takes the previous OutboundSessionInfo, and considers whether to create
// a new one. Also shares the key with any (new) devices in the room.
// Updates `session` to hold the final OutboundSessionInfo.
//
// returns a promise which resolves once the keyshare is successful.
function prepareSession(oldSession) {
session = oldSession;
if (session.sharePromise) {
// key share already in progress
return session.sharePromise;
}
// need to make a brand new session?
if (session && session.needsRotation(self._sessionRotationPeriodMsgs,
self._sessionRotationPeriodMs)
) {
console.log("Starting new megolm session because we need to rotate.");
session = null;
}
// no share in progress: check if we need to share with any devices
var prom = this._getDevicesInRoom(room).then(function(devicesInRoom) {
var shareMap = {};
// determine if we have shared with anyone we shouldn't have
if (session && session.sharedWithTooManyDevices(devicesInRoom)) {
session = null;
}
for (var userId in devicesInRoom) {
if (!session) {
session = self._prepareNewSession();
}
// now check if we need to share with any devices
const shareMap = {};
for (const userId in devicesInRoom) {
if (!devicesInRoom.hasOwnProperty(userId)) {
continue;
}
var userDevices = devicesInRoom[userId];
const userDevices = devicesInRoom[userId];
for (var deviceId in userDevices) {
for (const deviceId in userDevices) {
if (!userDevices.hasOwnProperty(deviceId)) {
continue;
}
var deviceInfo = userDevices[deviceId];
const deviceInfo = userDevices[deviceId];
if (deviceInfo.isBlocked()) {
continue;
}
var key = deviceInfo.getIdentityKey();
const key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
continue;
@@ -173,32 +220,37 @@ MegolmEncryption.prototype._ensureOutboundSession = function(room) {
}
return self._shareKeyWithDevices(
session, shareMap
session, shareMap,
);
}).finally(function() {
session.sharePromise = null;
}).then(function() {
return session;
});
}
session.sharePromise = prom;
return prom;
// helper which returns the session prepared by prepareSession
function returnSession() {
return session;
}
// first wait for the previous share to complete
const prom = this._setupPromise.then(prepareSession);
// _setupPromise resolves to `session` whether or not the share succeeds
this._setupPromise = prom.then(returnSession, returnSession);
// but we return a promise which only resolves if the share was successful.
return prom.then(returnSession);
};
/**
* @private
*
* @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);
MegolmEncryption.prototype._prepareNewSession = function() {
const session_id = this._olmDevice.createOutboundGroupSession();
const key = this._olmDevice.getOutboundGroupSessionKey(session_id);
this._olmDevice.addInboundGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, session_id,
key.key, {ed25519: this._olmDevice.deviceEd25519Key}
key.key, {ed25519: this._olmDevice.deviceEd25519Key},
);
return new OutboundSessionInfo(session_id);
@@ -216,10 +268,10 @@ MegolmEncryption.prototype._prepareNewSession = function(room) {
* message has been sent.
*/
MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUser) {
var self = this;
const self = this;
var key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
var payload = {
const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
const payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
@@ -227,29 +279,29 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUse
session_id: session.sessionId,
session_key: key.key,
chain_index: key.chain_index,
}
},
};
var contentMap = {};
const contentMap = {};
return olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser
this._olmDevice, this._baseApis, devicesByUser,
).then(function(devicemap) {
var haveTargets = false;
let haveTargets = false;
for (var userId in devicesByUser) {
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
var devicesToShareWith = devicesByUser[userId];
var sessionResults = devicemap[userId];
const devicesToShareWith = devicesByUser[userId];
const sessionResults = devicemap[userId];
for (var i = 0; i < devicesToShareWith.length; i++) {
var deviceInfo = devicesToShareWith[i];
var deviceId = deviceInfo.deviceId;
for (let i = 0; i < devicesToShareWith.length; i++) {
const deviceInfo = devicesToShareWith[i];
const deviceId = deviceInfo.deviceId;
var sessionResult = sessionResults[deviceId];
const sessionResult = sessionResults[deviceId];
if (!sessionResult.sessionId) {
// no session with this device, probably because there
// were no one-time keys.
@@ -266,10 +318,10 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUse
}
console.log(
"sharing keys with device " + userId + ":" + deviceId
"sharing keys with device " + userId + ":" + deviceId,
);
var encryptedContent = {
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: {},
@@ -282,7 +334,7 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUse
self._olmDevice,
userId,
deviceInfo,
payload
payload,
);
if (!contentMap[userId]) {
@@ -307,16 +359,16 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUse
// 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) {
for (const 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];
const devicesToShareWith = devicesByUser[userId];
for (let i = 0; i < devicesToShareWith.length; i++) {
const deviceInfo = devicesToShareWith[i];
session.sharedWithDevices[userId][deviceInfo.deviceId] =
key.chain_index;
}
@@ -329,24 +381,30 @@ MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUse
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
* @param {object} content 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 = {
const self = this;
return this._getDevicesInRoom(room).then(function(devicesInRoom) {
// check if any of these devices are not yet known to the user.
// if so, warn the user so they can verify or ignore.
self._checkForUnknownDevices(devicesInRoom);
return self._ensureOutboundSession(devicesInRoom);
}).then(function(session) {
const payloadJson = {
room_id: self._roomId,
type: eventType,
content: content
content: content,
};
var ciphertext = self._olmDevice.encryptGroupMessage(
session.sessionId, JSON.stringify(payloadJson)
const ciphertext = self._olmDevice.encryptGroupMessage(
session.sessionId, JSON.stringify(payloadJson),
);
var encryptedContent = {
const encryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext,
@@ -362,30 +420,38 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
};
/**
* @inheritdoc
* Checks the devices we're about to send to and see if any are entirely
* unknown to the user. If so, warn the user, and mark them as known to
* give the user a chance to go verify them before re-sending this message.
*
* @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
* @param {Object} devicesInRoom userId -> {deviceId -> object}
* devices we should shared the session with.
*/
MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembership) {
var newMembership = member.membership;
MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) {
const unknownDevices = {};
if (newMembership === 'join' || newMembership === 'invite') {
return;
Object.keys(devicesInRoom).forEach((userId)=>{
Object.keys(devicesInRoom[userId]).forEach((deviceId)=>{
const device = devicesInRoom[userId][deviceId];
if (device.isUnverified() && !device.isKnown()) {
if (!unknownDevices[userId]) {
unknownDevices[userId] = {};
}
unknownDevices[userId][deviceId] = device;
}
});
});
if (Object.keys(unknownDevices).length) {
// it'd be kind to pass unknownDevices up to the user in this error
throw new base.UnknownDeviceError(
"This room contains unknown devices which have not been verified. " +
"We strongly recommend you verify them before continuing.", unknownDevices);
}
// 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
* Get the list of unblocked devices for all users in the room
*
* @param {module:models/room} room
*
@@ -394,7 +460,7 @@ MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembers
*/
MegolmEncryption.prototype._getDevicesInRoom = function(room) {
// XXX what about rooms where invitees can see the content?
var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
const roomMembers = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
@@ -402,7 +468,36 @@ MegolmEncryption.prototype._getDevicesInRoom = function(room) {
// 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);
//
// XXX: what if the cache is stale, and the user left the room we had in
// common and then added new devices before joining this one? --Matthew
//
// yup, see https://github.com/vector-im/riot-web/issues/2305 --richvdh
return this._crypto.downloadKeys(roomMembers, false).then((devices) => {
// remove any blocked devices
for (const userId in devices) {
if (!devices.hasOwnProperty(userId)) {
continue;
}
const userDevices = devices[userId];
for (const deviceId in userDevices) {
if (!userDevices.hasOwnProperty(deviceId)) {
continue;
}
if (userDevices[deviceId].isBlocked() ||
(userDevices[deviceId].isUnverified() &&
(room.getBlacklistUnverifiedDevices() ||
this._crypto.getGlobalBlacklistUnverifiedDevices()))
) {
delete userDevices[deviceId];
}
}
}
return devices;
});
};
/**
@@ -428,14 +523,11 @@ utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
*
* @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();
const content = event.getWireContent();
if (!content.sender_key || !content.session_id ||
!content.ciphertext
@@ -443,34 +535,41 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
throw new base.DecryptionError("Missing fields in input");
}
var res;
let res;
try {
res = this._olmDevice.decryptGroupMessage(
event.getRoomId(), content.sender_key, content.session_id, content.ciphertext
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);
throw new base.DecryptionError(
e.toString(), {
session: content.sender_key + '|' + content.session_id,
},
);
}
if (res === null) {
// We've got a message for a session we don't have.
this._addEventToPendingList(event);
throw new base.DecryptionError(
"The sender's device has not sent us the keys for this message."
"The sender's device has not sent us the keys for this message.",
{
session: content.sender_key + '|' + content.session_id,
},
);
}
var payload = JSON.parse(res.result);
const 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
"Message intended for room " + payload.room_id,
);
}
@@ -487,8 +586,8 @@ MegolmDecryption.prototype.decryptEvent = function(event) {
* @param {module:models/event.MatrixEvent} event
*/
MegolmDecryption.prototype._addEventToPendingList = function(event) {
var content = event.getWireContent();
var k = content.sender_key + "|" + content.session_id;
const content = event.getWireContent();
const k = content.sender_key + "|" + content.session_id;
if (!this._pendingEvents[k]) {
this._pendingEvents[k] = [];
}
@@ -501,39 +600,71 @@ MegolmDecryption.prototype._addEventToPendingList = function(event) {
* @param {module:models/event.MatrixEvent} event key event
*/
MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
console.log("Adding key from ", event);
var content = event.getContent();
const content = event.getContent();
const senderKey = event.getSenderKey();
const sessionId = content.session_id;
if (!content.room_id ||
!content.session_id ||
!sessionId ||
!content.session_key
) {
console.error("key event is missing fields");
return;
}
if (!senderKey) {
console.error("key event has no sender key (not encrypted?)");
return;
}
console.log(`Adding key for megolm session ${senderKey}|${sessionId}`);
this._olmDevice.addInboundGroupSession(
content.room_id, event.getSenderKey(), content.session_id,
content.session_key, event.getKeysClaimed()
content.room_id, senderKey, sessionId,
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];
// have another go at decrypting events sent with this session.
this._retryDecryption(senderKey, sessionId);
};
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);
}
/**
* @inheritdoc
*
* @param {module:crypto/OlmDevice.MegolmSessionData} session
*/
MegolmDecryption.prototype.importRoomKey = function(session) {
this._olmDevice.importInboundGroupSession(session);
// have another go at decrypting events sent with this session.
this._retryDecryption(session.sender_key, session.session_id);
};
/**
* Have another go at decrypting events after we receive a key
*
* @private
* @param {String} senderKey
* @param {String} sessionId
*/
MegolmDecryption.prototype._retryDecryption = function(senderKey, sessionId) {
const k = senderKey + "|" + sessionId;
const pending = this._pendingEvents[k];
if (!pending) {
return;
}
delete this._pendingEvents[k];
for (let i = 0; i < pending.length; i++) {
try {
this.decryptEvent(pending[i]);
console.log("successful re-decryption of", pending[i]);
} catch (e) {
console.log("Still can't decrypt", pending[i], e.stack || e);
}
}
};
base.registerAlgorithm(
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption,
);
@@ -20,15 +20,15 @@ limitations under the License.
*
* @module crypto/algorithms/olm
*/
var q = require('q');
const q = require('q');
var utils = require("../../utils");
var olmlib = require("../olmlib");
var DeviceInfo = require("../deviceinfo");
var DeviceVerification = DeviceInfo.DeviceVerification;
const utils = require("../../utils");
const olmlib = require("../olmlib");
const DeviceInfo = require("../deviceinfo");
const DeviceVerification = DeviceInfo.DeviceVerification;
var base = require("./base");
const base = require("./base");
/**
* Olm encryption implementation
@@ -63,8 +63,8 @@ OlmEncryption.prototype._ensureSession = function(roomMembers) {
return q();
}
var self = this;
this._prepPromise = self._crypto.downloadKeys(roomMembers, true).then(function(res) {
const self = this;
this._prepPromise = self._crypto.downloadKeys(roomMembers).then(function(res) {
return self._crypto.ensureOlmSessionsForUsers(roomMembers);
}).then(function() {
self._sessionPrepared = true;
@@ -79,7 +79,7 @@ OlmEncryption.prototype._ensureSession = function(roomMembers) {
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
* @param {object} content plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
@@ -89,31 +89,31 @@ OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
// 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) {
const users = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
var self = this;
const self = this;
return this._ensureSession(users).then(function() {
var payloadFields = {
const payloadFields = {
room_id: room.roomId,
type: eventType,
content: content,
};
var encryptedContent = {
const 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 (let i = 0; i < users.length; ++i) {
const userId = users[i];
const devices = self._crypto.getStoredDevicesForUser(userId);
for (var j = 0; j < devices.length; ++j) {
var deviceInfo = devices[j];
var key = deviceInfo.getIdentityKey();
for (let j = 0; j < devices.length; ++j) {
const deviceInfo = devices[j];
const key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
continue;
@@ -126,7 +126,7 @@ OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
self._userId, self._deviceId, self._olmDevice,
userId, deviceInfo, payloadFields
userId, deviceInfo, payloadFields,
);
}
}
@@ -157,9 +157,9 @@ utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
* problem decrypting the event
*/
OlmDecryption.prototype.decryptEvent = function(event) {
var content = event.getWireContent();
var deviceKey = content.sender_key;
var ciphertext = content.ciphertext;
const content = event.getWireContent();
const deviceKey = content.sender_key;
const ciphertext = content.ciphertext;
if (!ciphertext) {
throw new base.DecryptionError("Missing ciphertext");
@@ -168,41 +168,37 @@ OlmDecryption.prototype.decryptEvent = function(event) {
if (!(this._olmDevice.deviceCurve25519Key in ciphertext)) {
throw new base.DecryptionError("Not included in recipients");
}
var message = ciphertext[this._olmDevice.deviceCurve25519Key];
var payloadString;
const message = ciphertext[this._olmDevice.deviceCurve25519Key];
let 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", {
sender: deviceKey,
err: e,
},
);
throw new base.DecryptionError("Bad Encrypted Message");
}
var payload = JSON.parse(payloadString);
const 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
"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"
if (payload.recipient_keys.ed25519 != this._olmDevice.deviceEd25519Key) {
throw new base.DecryptionError(
"Message not intended for this device", {
intended: payload.recipient_keys.ed25519,
our_key: this._olmDevice.deviceEd25519Key,
},
);
throw new base.DecryptionError("Message not intended for this device");
}
// check that the original sender matches what the homeserver told us, to
@@ -210,23 +206,19 @@ OlmDecryption.prototype.decryptEvent = function(event) {
// (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
"Message forwarded from " + payload.sender, {
reported_sender: event.getSender(),
},
);
}
// 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
"Message intended for room " + payload.room_id, {
reported_room: event.room_id,
},
);
}
@@ -243,24 +235,24 @@ OlmDecryption.prototype.decryptEvent = function(event) {
* @return {string} payload, if decrypted successfully.
*/
OlmDecryption.prototype._decryptMessage = function(theirDeviceIdentityKey, message) {
var sessionIds = this._olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
const sessionIds = this._olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
// try each session in turn.
var decryptionErrors = {};
for (var i = 0; i < sessionIds.length; i++) {
var sessionId = sessionIds[i];
const decryptionErrors = {};
for (let i = 0; i < sessionIds.length; i++) {
const sessionId = sessionIds[i];
try {
var payload = this._olmDevice.decryptMessage(
theirDeviceIdentityKey, sessionId, message.type, message.body
const payload = this._olmDevice.decryptMessage(
theirDeviceIdentityKey, sessionId, message.type, message.body,
);
console.log(
"Decrypted Olm message from " + theirDeviceIdentityKey +
" with session " + sessionId
" with session " + sessionId,
);
return payload;
} catch (e) {
var foundSession = this._olmDevice.matchesSession(
theirDeviceIdentityKey, sessionId, message.type, message.body
const foundSession = this._olmDevice.matchesSession(
theirDeviceIdentityKey, sessionId, message.type, message.body,
);
if (foundSession) {
@@ -268,7 +260,7 @@ OlmDecryption.prototype._decryptMessage = function(theirDeviceIdentityKey, messa
// session, so it should have worked.
throw new Error(
"Error decrypting prekey message with existing session id " +
sessionId + ": " + e.message
sessionId + ": " + e.message,
);
}
@@ -288,29 +280,29 @@ OlmDecryption.prototype._decryptMessage = function(theirDeviceIdentityKey, messa
throw new Error(
"Error decrypting non-prekey message with existing sessions: " +
JSON.stringify(decryptionErrors)
JSON.stringify(decryptionErrors),
);
}
// prekey message which doesn't match any existing sessions: make a new
// session.
var res;
let res;
try {
res = this._olmDevice.createInboundSession(
theirDeviceIdentityKey, message.type, message.body
theirDeviceIdentityKey, message.type, message.body,
);
} catch (e) {
decryptionErrors["(new)"] = e.message;
throw new Error(
"Error decrypting prekey message: " +
JSON.stringify(decryptionErrors)
JSON.stringify(decryptionErrors),
);
}
console.log(
"created new inbound Olm session ID " +
res.session_id + " with " + theirDeviceIdentityKey
res.session_id + " with " + theirDeviceIdentityKey,
);
return res.payload;
};
@@ -34,7 +34,11 @@ limitations under the License.
* &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
* whether the device has been verified/blocked by the user
*
* @property {boolean} known
* whether the user knows of this device's existence (useful when warning
* the user that a user has added new devices)
*
* @property {Object} unsigned additional data from the homeserver
*
@@ -50,6 +54,7 @@ function DeviceInfo(deviceId) {
this.algorithms = [];
this.keys = {};
this.verified = DeviceVerification.UNVERIFIED;
this.known = false;
this.unsigned = {};
}
@@ -62,8 +67,8 @@ function DeviceInfo(deviceId) {
* @return {module:crypto~DeviceInfo} new DeviceInfo
*/
DeviceInfo.fromStorage = function(obj, deviceId) {
var res = new DeviceInfo(deviceId);
for (var prop in obj) {
const res = new DeviceInfo(deviceId);
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
res[prop] = obj[prop];
}
@@ -81,6 +86,7 @@ DeviceInfo.prototype.toStorage = function() {
algorithms: this.algorithms,
keys: this.keys,
verified: this.verified,
known: this.known,
unsigned: this.unsigned,
};
};
@@ -130,6 +136,24 @@ DeviceInfo.prototype.isVerified = function() {
return this.verified == DeviceVerification.VERIFIED;
};
/**
* Returns true if this device is unverified
*
* @return {Boolean} true if unverified
*/
DeviceInfo.prototype.isUnverified = function() {
return this.verified == DeviceVerification.UNVERIFIED;
};
/**
* Returns true if the user knows about this device's existence
*
* @return {Boolean} true if known
*/
DeviceInfo.prototype.isKnown = function() {
return this.known == true;
};
/**
* @enum
*/
@@ -139,7 +163,7 @@ DeviceInfo.DeviceVerification = {
BLOCKED: -1,
};
var DeviceVerification = DeviceInfo.DeviceVerification;
const DeviceVerification = DeviceInfo.DeviceVerification;
/** */
module.exports = DeviceInfo;
File diff suppressed because it is too large Load Diff
+53 -48
View File
@@ -20,10 +20,10 @@ limitations under the License.
* Utilities common to olm encryption algorithms
*/
var q = require('q');
var anotherjson = require('another-json');
const q = require('q');
const anotherjson = require('another-json');
var utils = require("../utils");
const utils = require("../utils");
/**
* matrix algorithm tag for olm
@@ -52,10 +52,10 @@ module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
module.exports.encryptMessageForDevice = function(
resultsObject,
ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice,
payloadFields
payloadFields,
) {
var deviceKey = recipientDevice.getIdentityKey();
var sessionId = olmDevice.getSessionIdForDevice(deviceKey);
const deviceKey = recipientDevice.getIdentityKey();
const 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.
@@ -64,10 +64,10 @@ module.exports.encryptMessageForDevice = function(
console.log(
"Using sessionid " + sessionId + " for device " +
recipientUserId + ":" + recipientDevice.deviceId
recipientUserId + ":" + recipientDevice.deviceId,
);
var payload = {
const payload = {
sender: ourUserId,
sender_device: ourDeviceId,
@@ -100,7 +100,7 @@ module.exports.encryptMessageForDevice = function(
utils.extend(payload, payloadFields);
resultsObject[deviceKey] = olmDevice.encryptMessage(
deviceKey, sessionId, JSON.stringify(payload)
deviceKey, sessionId, JSON.stringify(payload),
);
};
@@ -119,22 +119,24 @@ module.exports.encryptMessageForDevice = function(
* {@link module:crypto~OlmSessionResult}
*/
module.exports.ensureOlmSessionsForDevices = function(
olmDevice, baseApis, devicesByUser
olmDevice, baseApis, devicesByUser,
) {
var devicesWithoutSession = [
const devicesWithoutSession = [
// [userId, deviceId], ...
];
var result = {};
const result = {};
for (var userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) { continue; }
for (const 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);
const devices = devicesByUser[userId];
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
const sessionId = olmDevice.getSessionIdForDevice(key);
if (sessionId === null) {
devicesWithoutSession.push([userId, deviceId]);
}
@@ -155,25 +157,28 @@ module.exports.ensureOlmSessionsForDevices = function(
//
// That should eventually resolve itself, but it's poor form.
var oneTimeKeyAlgorithm = "signed_curve25519";
const oneTimeKeyAlgorithm = "signed_curve25519";
return baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm
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;
const otk_res = res.one_time_keys || {};
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
const userRes = otk_res[userId] || {};
const devices = devicesByUser[userId];
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
if (result[userId][deviceId].sessionId) {
// we already have a result for this device
continue;
}
var deviceRes = userRes[deviceId] || {};
var oneTimeKey = null;
for (var keyId in deviceRes) {
const deviceRes = userRes[deviceId] || {};
let oneTimeKey = null;
for (const keyId in deviceRes) {
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
oneTimeKey = deviceRes[keyId];
}
@@ -182,13 +187,13 @@ module.exports.ensureOlmSessionsForDevices = function(
if (!oneTimeKey) {
console.warn(
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
") for device " + userId + ":" + deviceId
") for device " + userId + ":" + deviceId,
);
continue;
}
var sid = _verifyKeyAndStartSession(
olmDevice, oneTimeKey, userId, deviceInfo
const sid = _verifyKeyAndStartSession(
olmDevice, oneTimeKey, userId, deviceInfo,
);
result[userId][deviceId].sessionId = sid;
}
@@ -199,24 +204,24 @@ module.exports.ensureOlmSessionsForDevices = function(
function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
var deviceId = deviceInfo.deviceId;
const deviceId = deviceInfo.deviceId;
try {
_verifySignature(
olmDevice, oneTimeKey, userId, deviceId,
deviceInfo.getFingerprint()
deviceInfo.getFingerprint(),
);
} catch (e) {
console.error(
"Unable to verify signature on one-time key for device " +
userId + ":" + deviceId + ":", e
userId + ":" + deviceId + ":", e,
);
return null;
}
var sid;
let sid;
try {
sid = olmDevice.createOutboundSession(
deviceInfo.getIdentityKey(), oneTimeKey.key
deviceInfo.getIdentityKey(), oneTimeKey.key,
);
} catch (e) {
// possibly a bad key
@@ -245,13 +250,13 @@ function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
*
* @param {string} signingKey base64-ed ed25519 public key
*/
var _verifySignature = module.exports.verifySignature = function(
olmDevice, obj, signingUserId, signingDeviceId, signingKey
const _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];
const signKeyId = "ed25519:" + signingDeviceId;
const signatures = obj.signatures || {};
const userSigs = signatures[signingUserId] || {};
const signature = userSigs[signKeyId];
if (!signature) {
throw Error("No signature");
}
@@ -260,9 +265,9 @@ var _verifySignature = module.exports.verifySignature = function(
// anotherjson
delete obj.unsigned;
delete obj.signatures;
var json = anotherjson.stringify(obj);
const json = anotherjson.stringify(obj);
olmDevice.verifySignature(
signingKey, json, signature
signingKey, json, signature,
);
};
@@ -27,10 +27,9 @@ limitations under the License.
*/
function _matches_wildcard(actual_value, filter_value) {
if (filter_value.endsWith("*")) {
var type_prefix = filter_value.slice(0, -1);
const type_prefix = filter_value.slice(0, -1);
return actual_value.substr(0, type_prefix.length) === type_prefix;
}
else {
} else {
return actual_value === filter_value;
}
}
@@ -44,7 +43,7 @@ function _matches_wildcard(actual_value, filter_value) {
* 'Filters' are referred to as 'FilterCollections'.
*
* @constructor
* @param {Object} the definition of this filter JSON, e.g. { 'contains_url': true }
* @param {Object} filter_json the definition of this filter JSON, e.g. { 'contains_url': true }
*/
function FilterComponent(filter_json) {
this.filter_json = filter_json;
@@ -71,7 +70,7 @@ FilterComponent.prototype.check = function(event) {
event.getRoomId(),
event.getSender(),
event.getType(),
event.getContent() ? event.getContent().url !== undefined : false
event.getContent() ? event.getContent().url !== undefined : false,
);
};
@@ -84,24 +83,29 @@ FilterComponent.prototype.check = function(event) {
* @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); },
function(room_id, sender, event_type, contains_url) {
const 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;
const 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];
const match_func = literal_keys[name];
const not_name = "not_" + name;
const disallowed_values = self[not_name];
if (disallowed_values.map(match_func)) {
return false;
}
var allowed_values = self[name];
const allowed_values = self[name];
if (allowed_values) {
if (!allowed_values.map(match_func)) {
return false;
@@ -109,7 +113,7 @@ FilterComponent.prototype._checkFields =
}
});
var contains_url_filter = this.filter_json.contains_url;
const contains_url_filter = this.filter_json.contains_url;
if (contains_url_filter !== undefined) {
if (contains_url_filter !== contains_url) {
return false;
+8 -8
View File
@@ -18,7 +18,7 @@ limitations under the License.
* @module filter
*/
var FilterComponent = require("./filter-component");
const FilterComponent = require("./filter-component");
/**
* @param {Object} obj
@@ -26,9 +26,9 @@ var FilterComponent = require("./filter-component");
* @param {*} val
*/
function setProp(obj, keyNesting, val) {
var nestedKeys = keyNesting.split(".");
var currentObj = obj;
for (var i = 0; i < (nestedKeys.length - 1); i++) {
const nestedKeys = keyNesting.split(".");
let currentObj = obj;
for (let i = 0; i < (nestedKeys.length - 1); i++) {
if (!currentObj[nestedKeys[i]]) {
currentObj[nestedKeys[i]] = {};
}
@@ -106,10 +106,10 @@ Filter.prototype.setDefinition = function(definition) {
// "event_fields": ["type", "content", "sender"]
// }
var room_filter_json = definition.room;
const room_filter_json = definition.room;
// consider the top level rooms/not_rooms filter
var room_filter_fields = {};
const room_filter_fields = {};
if (room_filter_json) {
if (room_filter_json.rooms) {
room_filter_fields.rooms = room_filter_json.rooms;
@@ -123,7 +123,7 @@ Filter.prototype.setDefinition = function(definition) {
this._room_filter = new FilterComponent(room_filter_fields);
this._room_timeline_filter = new FilterComponent(
room_filter_json ? (room_filter_json.timeline || {}) : {}
room_filter_json ? (room_filter_json.timeline || {}) : {},
);
// don't bother porting this from synapse yet:
@@ -183,7 +183,7 @@ Filter.prototype.setIncludeLeaveRooms = function(includeLeave) {
* @return {Filter}
*/
Filter.fromJson = function(userId, filterId, jsonObj) {
var filter = new Filter(userId, filterId);
const filter = new Filter(userId, filterId);
filter.setDefinition(jsonObj);
return filter;
};
+106 -85
View File
@@ -18,13 +18,13 @@ limitations under the License.
* This is an internal module. See {@link MatrixHttpApi} for the public class.
* @module http-api
*/
var q = require("q");
var utils = require("./utils");
const q = require("q");
const utils = require("./utils");
// we use our own implementation of setTimeout, so that if we get suspended in
// the middle of a /sync, we cancel the sync as soon as we awake, rather than
// waiting for the delay to elapse.
var callbacks = require("./realtime-callbacks");
const callbacks = require("./realtime-callbacks");
/*
TODO:
@@ -70,8 +70,10 @@ module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
*
* @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
* @param {Object=} opts.extraParams Optional. Extra query parameters to send on
* requests.
* @param {Number=} opts.localTimeoutMs The default maximum amount of time to wait
* before timing out the request. If not specified, there is no timeout.
*/
module.exports.MatrixHttpApi = function MatrixHttpApi(event_emitter, opts) {
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
@@ -89,13 +91,13 @@ module.exports.MatrixHttpApi.prototype = {
* path and query parameters respectively.
*/
getContentUri: function() {
var params = {
access_token: this.opts.accessToken
const params = {
access_token: this.opts.accessToken,
};
return {
base: this.opts.baseUrl,
path: "/_matrix/media/v1/upload",
params: params
params: params,
};
},
@@ -143,16 +145,16 @@ module.exports.MatrixHttpApi.prototype = {
// 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;
const contentType = opts.type || file.type || 'application/octet-stream';
const 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;
const body = file.stream ? file.stream : file;
// backwards-compatibility hacks where we used to do different things
// between browser and node.
var rawResponse = opts.rawResponse;
let rawResponse = opts.rawResponse;
if (rawResponse === undefined) {
if (global.XMLHttpRequest) {
rawResponse = false;
@@ -161,20 +163,20 @@ module.exports.MatrixHttpApi.prototype = {
"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."
"to change this behaviour now.",
);
rawResponse = true;
}
}
var onlyContentUri = opts.onlyContentUri;
let 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."
"opts.onlyContentUri=false to change this behaviour now.",
);
onlyContentUri = true;
} else {
@@ -190,17 +192,17 @@ module.exports.MatrixHttpApi.prototype = {
// (browser-request doesn't support progress either, which is also kind
// of important here)
var upload = { loaded: 0, total: 0 };
var promise;
const upload = { loaded: 0, total: 0 };
let 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;
let bodyParser = null;
if (!rawResponse) {
bodyParser = function(rawBody) {
var body = JSON.parse(rawBody);
let body = JSON.parse(rawBody);
if (onlyContentUri) {
body = body.content_uri;
if (body === undefined) {
@@ -212,12 +214,12 @@ module.exports.MatrixHttpApi.prototype = {
}
if (global.XMLHttpRequest) {
var defer = q.defer();
var xhr = new global.XMLHttpRequest();
const defer = q.defer();
const xhr = new global.XMLHttpRequest();
upload.xhr = xhr;
var cb = requestCallback(defer, opts.callback, this.opts.onlyData);
const cb = requestCallback(defer, opts.callback, this.opts.onlyData);
var timeout_fn = function() {
const timeout_fn = function() {
xhr.abort();
cb(new Error('Timeout'));
};
@@ -255,7 +257,7 @@ module.exports.MatrixHttpApi.prototype = {
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
defer.notify(ev);
});
var url = this.opts.baseUrl + "/_matrix/media/v1/upload";
let url = this.opts.baseUrl + "/_matrix/media/v1/upload";
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
url += "&filename=" + encodeURIComponent(fileName);
@@ -267,7 +269,7 @@ module.exports.MatrixHttpApi.prototype = {
// dirty hack (as per _request) to allow the upload to be cancelled.
promise.abort = xhr.abort.bind(xhr);
} else {
var queryParams = {
const queryParams = {
filename: fileName,
};
@@ -277,15 +279,15 @@ module.exports.MatrixHttpApi.prototype = {
headers: {"Content-Type": contentType},
json: false,
bodyParser: bodyParser,
}
},
);
}
var self = this;
const self = this;
// remove the upload from the list on completion
var promise0 = promise.finally(function() {
for (var i = 0; i < self.uploads.length; ++i) {
const promise0 = promise.finally(function() {
for (let i = 0; i < self.uploads.length; ++i) {
if (self.uploads[i] === upload) {
self.uploads.splice(i, 1);
return;
@@ -315,20 +317,20 @@ module.exports.MatrixHttpApi.prototype = {
},
idServerRequest: function(callback, method, path, params, prefix) {
var fullUri = this.opts.idBaseUrl + prefix + path;
const fullUri = this.opts.idBaseUrl + prefix + path;
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
"Expected callback to be a function but got " + typeof callback,
);
}
var opts = {
const opts = {
uri: fullUri,
method: method,
withCredentials: false,
json: false,
_matrix_opts: this.opts
_matrix_opts: this.opts,
};
if (method == 'GET') {
opts.qs = params;
@@ -336,10 +338,10 @@ module.exports.MatrixHttpApi.prototype = {
opts.form = params;
}
var defer = q.defer();
const defer = q.defer();
this.opts.request(
opts,
requestCallback(defer, callback, this.opts.onlyData)
requestCallback(defer, callback, this.opts.onlyData),
);
// ID server does not always take JSON, so we can't use requests' 'json'
// option as we do with the home server, but it does return JSON, so
@@ -387,11 +389,11 @@ module.exports.MatrixHttpApi.prototype = {
queryParams.access_token = this.opts.accessToken;
}
var request_promise = this.request(
callback, method, path, queryParams, data, opts
const request_promise = this.request(
callback, method, path, queryParams, data, opts,
);
var self = this;
const self = this;
request_promise.catch(function(err) {
if (err.errcode == 'M_UNKNOWN_TOKEN') {
self.event_emitter.emit("Session.logged_out");
@@ -435,11 +437,11 @@ module.exports.MatrixHttpApi.prototype = {
*/
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;
const prefix = opts.prefix !== undefined ? opts.prefix : this.opts.prefix;
const fullUri = this.opts.baseUrl + prefix + path;
return this.requestOtherUrl(
callback, method, fullUri, queryParams, data, opts
callback, method, fullUri, queryParams, data, opts,
);
},
@@ -474,7 +476,7 @@ module.exports.MatrixHttpApi.prototype = {
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
}
},
);
},
@@ -509,7 +511,7 @@ module.exports.MatrixHttpApi.prototype = {
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
}
},
);
},
@@ -549,12 +551,12 @@ module.exports.MatrixHttpApi.prototype = {
} else if (isFinite(opts)) {
// opts used to be localTimeoutMs
opts = {
localTimeoutMs: opts
localTimeoutMs: opts,
};
}
return this._request(
callback, method, uri, queryParams, data, opts
callback, method, uri, queryParams, data, opts,
);
},
@@ -570,7 +572,7 @@ module.exports.MatrixHttpApi.prototype = {
* @return {string} URL
*/
getUrl: function(path, queryParams, prefix) {
var queryString = "";
let queryString = "";
if (queryParams) {
queryString = "?" + utils.encodeParams(queryParams);
}
@@ -594,7 +596,7 @@ module.exports.MatrixHttpApi.prototype = {
* @param {object=} opts.headers extra request headers
*
* @param {number=} opts.localTimeoutMs client-side timeout for the
* request. No timeout if undefined.
* request. Default timeout if falsy.
*
* @param {function=} opts.bodyParser function to parse the body of the
* response before passing it to the promise and callback.
@@ -606,42 +608,51 @@ module.exports.MatrixHttpApi.prototype = {
_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
"Expected callback to be a function but got " + typeof callback,
);
}
opts = opts || {};
var self = this;
const self = this;
if (this.opts.extraParams) {
for (var key in this.opts.extraParams) {
if (!this.opts.extraParams.hasOwnProperty(key)) { continue; }
for (const 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;
const json = opts.json === undefined ? true : opts.json;
var defer = q.defer();
const defer = q.defer();
var timeoutId;
var timedOut = false;
var req;
var localTimeoutMs = opts.localTimeoutMs;
if (localTimeoutMs) {
timeoutId = callbacks.setTimeout(function() {
timedOut = true;
if (req && req.abort) {
req.abort();
let timeoutId;
let timedOut = false;
let req;
const localTimeoutMs = opts.localTimeoutMs || this.opts.localTimeoutMs;
const resetTimeout = () => {
if (localTimeoutMs) {
if (timeoutId) {
callbacks.clearTimeout(timeoutId);
}
defer.reject(new module.exports.MatrixError({
error: "Locally timed out waiting for a response",
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
timeout: localTimeoutMs
}));
}, localTimeoutMs);
}
timeoutId = callbacks.setTimeout(function() {
timedOut = true;
if (req && req.abort) {
req.abort();
}
defer.reject(new module.exports.MatrixError({
error: "Locally timed out waiting for a response",
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
timeout: localTimeoutMs,
}));
}, localTimeoutMs);
}
};
resetTimeout();
var reqPromise = defer.promise;
const reqPromise = defer.promise;
try {
req = this.opts.request(
@@ -654,7 +665,7 @@ module.exports.MatrixHttpApi.prototype = {
json: json,
timeout: localTimeoutMs,
headers: opts.headers || {},
_matrix_opts: this.opts
_matrix_opts: this.opts,
},
function(err, response, body) {
if (localTimeoutMs) {
@@ -666,29 +677,40 @@ module.exports.MatrixHttpApi.prototype = {
// if json is falsy, we won't parse any error response, so need
// to do so before turning it into a MatrixError
var parseErrorJson = !json;
var handlerFn = requestCallback(
const parseErrorJson = !json;
const handlerFn = requestCallback(
defer, callback, self.opts.onlyData,
parseErrorJson,
opts.bodyParser
opts.bodyParser,
);
handlerFn(err, response, body);
}
},
);
if (req && req.abort) {
if (req) {
// This will only work in a browser, where opts.request is the
// `browser-request` import. Currently `request` does not support progress
// updates - see https://github.com/request/request/pull/2346.
// `browser-request` returns an XHRHttpRequest which exposes `onprogress`
if ('onprogress' in req) {
req.onprogress = (e) => {
// Prevent the timeout from rejecting the deferred promise if progress is
// seen with the request
resetTimeout();
};
}
// FIXME: This is EVIL, but I can't think of a better way to expose
// abort() operations on underlying HTTP requests :(
reqPromise.abort = req.abort.bind(req);
if (req.abort) reqPromise.abort = req.abort.bind(req);
}
}
catch (ex) {
} catch (ex) {
defer.reject(ex);
if (callback) {
callback(ex);
}
}
return reqPromise;
}
},
};
/*
@@ -702,9 +724,9 @@ module.exports.MatrixHttpApi.prototype = {
* If parseErrorJson is true, we will JSON.parse the body if we get a 4xx error.
*
*/
var requestCallback = function(
const requestCallback = function(
defer, userDefinedCallback, onlyData,
parseErrorJson, bodyParser
parseErrorJson, bodyParser,
) {
userDefinedCallback = userDefinedCallback || function() {};
@@ -731,12 +753,11 @@ var requestCallback = function(
if (err) {
defer.reject(err);
userDefinedCallback(err);
}
else {
var res = {
} else {
const res = {
code: response.statusCode,
headers: response.headers,
data: body
data: body,
};
defer.resolve(onlyData ? body : res);
userDefinedCallback(null, onlyData ? body : res);
+24
View File
@@ -0,0 +1,24 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Separate exports file for the indexeddb web worker, which is designed
* to be used separately
*/
/** The {@link module:indexeddb-store-worker~IndexedDBStoreWorker} class. */
module.exports.IndexedDBStoreWorker = require("./store/indexeddb-store-worker.js");
+422
View File
@@ -0,0 +1,422 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/** @module interactive-auth */
const q = require("q");
const url = require("url");
const utils = require("./utils");
const EMAIL_STAGE_TYPE = "m.login.email.identity";
const MSISDN_STAGE_TYPE = "m.login.msisdn";
/**
* 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.matrixClient A matrix client to use for the auth process
*
* @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?, bool?): module:client.Promise} opts.doRequest
* called with the new auth dict to submit the request and a flag set
* to true if this request is a background request. Should return a
* promise which resolves to the successful response or rejects with a
* MatrixError.
*
* @param {function(string, object?)} opts.stateUpdated
* called when the status of the UI auth changes, ie. when the state of
* an auth stage changes of when the auth flow moves to a new stage.
* The arguments are: the login type (eg m.login.password); and an object
* which is either an error or an informational object specific to the
* login type. If the 'errcode' key is defined, the object is an error,
* and has keys:
* errcode: string, the textual error code, eg. M_UNKNOWN
* error: string, human readable string describing the error
*
* The login type specific objects are as follows:
* m.login.email.identity:
* * emailSid: string, the sid of the active email auth session
*
* @param {object?} opts.inputs Inputs provided by the user and used by different
* stages of the auto process. The inputs provided will affect what flow is chosen.
*
* @param {string?} opts.inputs.emailAddress An email address. If supplied, a flow
* using email verification will be chosen.
*
* @param {string?} opts.inputs.phoneCountry An ISO two letter country code. Gives
* the country that opts.phoneNumber should be resolved relative to.
*
* @param {string?} opts.inputs.phoneNumber A phone number. If supplied, a flow
* using phone number validation will be chosen.
*
* @param {string?} opts.sessionId If resuming an existing interactive auth session,
* the sessionId of that session.
*
* @param {string?} opts.clientSecret If resuming an existing interactive auth session,
* the client secret for that session
*
* @param {string?} opts.emailSid If returning from having completed m.login.email.identity
* auth, the sid for the email verification session.
*
*/
function InteractiveAuth(opts) {
this._matrixClient = opts.matrixClient;
this._data = opts.authData || {};
this._requestCallback = opts.doRequest;
// startAuthStage included for backwards compat
this._stateUpdatedCallback = opts.stateUpdated || opts.startAuthStage;
this._completionDeferred = null;
this._inputs = opts.inputs || {};
if (opts.sessionId) this._data.session = opts.sessionId;
this._clientSecret = opts.clientSecret || this._matrixClient.generateClientSecret();
this._emailSid = opts.emailSid;
if (this._emailSid === undefined) this._emailSid = null;
this._currentStage = 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. Rejects with NoAuthFlowFoundError if
* no suitable authentication flow can be found
*/
attemptAuth: function() {
this._completionDeferred = q.defer();
// wrap in a promise so that if _startNextAuthStage
// throws, it rejects the promise in a consistent way
return q().then(() => {
// if we have no flows, try a request (we'll have
// just a session ID in _data if resuming)
if (!this._data.flows) {
this._doRequest(this._data);
} else {
this._startNextAuthStage();
}
return this._completionDeferred.promise;
});
},
/**
* Poll to check if the auth session or current stage has been
* completed out-of-band. If so, the attemptAuth promise will
* be resolved.
*/
poll: function() {
if (!this._data.session) return;
let authDict = {};
if (this._currentStage == EMAIL_STAGE_TYPE) {
// The email can be validated out-of-band, but we need to provide the
// creds so the HS can go & check it.
if (this._emailSid) {
const idServerParsedUrl = url.parse(
this._matrixClient.getIdentityServerUrl(),
);
authDict = {
type: EMAIL_STAGE_TYPE,
threepid_creds: {
sid: this._emailSid,
client_secret: this._clientSecret,
id_server: idServerParsedUrl.host,
},
};
}
}
this.submitAuthDict(authDict, true);
},
/**
* get the auth session ID
*
* @return {string} session id
*/
getSessionId: function() {
return this._data ? this._data.session : undefined;
},
/**
* get the client secret used for validation sessions
* with the ID server.
*
* @return {string} client secret
*/
getClientSecret: function() {
return this._clientSecret;
},
/**
* get the server params for a given stage
*
* @param {string} loginType login type for the stage
* @return {object?} any parameters from the server for this stage
*/
getStageParams: function(loginType) {
let 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.
* @param {bool} background If true, this request failing will not result
* in the attemptAuth promise being rejected. This can be set to true
* for requests that just poll to see if auth has been completed elsewhere.
*/
submitAuthDict: function(authData, background) {
if (!this._completionDeferred) {
throw new Error("submitAuthDict() called before attemptAuth()");
}
// use the sessionid from the last request.
const auth = {
session: this._data.session,
};
utils.extend(auth, authData);
this._doRequest(auth, background);
},
/**
* Gets the sid for the email validation session
* Specific to m.login.email.identity
*
* @returns {string} The sid of the email auth session
*/
getEmailSid: function() {
return this._emailSid;
},
/**
* Sets the sid for the email validation session
* This must be set in order to successfully poll for completion
* of the email validation.
* Specific to m.login.email.identity
*
* @param {string} sid The sid for the email validation session
*/
setEmailSid: function(sid) {
this._emailSid = sid;
},
/**
* Fire off a request, and either resolve the promise, or call
* startAuthStage.
*
* @private
* @param {object?} auth new auth dict, including session id
* @param {bool?} background If true, this request is a background poll, so it
* failing will not result in the attemptAuth promise being rejected.
* This can be set to true for requests that just poll to see if auth has
* been completed elsewhere.
*/
_doRequest: function(auth, background) {
const 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)
let prom;
try {
prom = this._requestCallback(auth, background);
} catch (e) {
prom = q.reject(e);
}
prom = prom.then(
function(result) {
console.log("result from request: ", result);
self._completionDeferred.resolve(result);
}, function(error) {
// sometimes UI auth errors don't come with flows
const errorFlows = error.data ? error.data.flows : null;
const haveFlows = Boolean(self._data.flows) || Boolean(errorFlows);
if (error.httpStatus !== 401 || !error.data || !haveFlows) {
// doesn't look like an interactive-auth failure. fail the whole lot.
throw error;
}
// if the error didn't come with flows, completed flows or session ID,
// copy over the ones we have. Synapse sometimes sends responses without
// any UI auth data (eg. when polling for email validation, if the email
// has not yet been validated). This appears to be a Synapse bug, which
// we workaround here.
if (!error.data.flows && !error.data.completed && !error.data.session) {
error.data.flows = self._data.flows;
error.data.completed = self._data.completed;
error.data.session = self._data.session;
}
self._data = error.data;
self._startNextAuthStage();
},
);
if (!background) {
prom = prom.catch(this._completionDeferred.reject);
} else {
// We ignore all failures here (even non-UI auth related ones)
// since we don't want to suddenly fail if the internet connection
// had a blip whilst we were polling
prom = prom.catch((error) => {
console.log("Ignoring error from UI auth: " + error);
});
}
prom.done();
},
/**
* Pick the next stage and call the callback
*
* @private
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
*/
_startNextAuthStage: function() {
const nextStage = this._chooseStage();
if (!nextStage) {
throw new Error("No incomplete flows from the server");
}
this._currentStage = nextStage;
if (this._data.errcode || this._data.error) {
this._stateUpdatedCallback(nextStage, {
errcode: this._data.errcode || "",
error: this._data.error || "",
});
return;
}
const stageStatus = {};
if (nextStage == EMAIL_STAGE_TYPE) {
stageStatus.emailSid = this._emailSid;
}
this._stateUpdatedCallback(nextStage, stageStatus);
},
/**
* Pick the next auth stage
*
* @private
* @return {string?} login type
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
*/
_chooseStage: function() {
const flow = this._chooseFlow();
console.log("Active flow => %s", JSON.stringify(flow));
const nextStage = this._firstUncompletedStage(flow);
console.log("Next stage: %s", nextStage);
return nextStage;
},
/**
* Pick one of the flows from the returned list
* If a flow using all of the inputs is found, it will
* be returned, otherwise, null will be returned.
*
* Only flows using all given inputs are chosen because it
* is likley to be surprising if the user provides a
* credential and it is not used. For example, for registration,
* this could result in the email not being used which would leave
* the account with no means to reset a password.
*
* @private
* @return {object} flow
* @throws {NoAuthFlowFoundError} If no suitable authentication flow can be found
*/
_chooseFlow: function() {
const flows = this._data.flows || [];
// we've been given an email or we've already done an email part
const haveEmail = Boolean(this._inputs.emailAddress) || Boolean(this._emailSid);
const haveMsisdn = (
Boolean(this._inputs.phoneCountry) &&
Boolean(this._inputs.phoneNumber)
);
for (const flow of flows) {
let flowHasEmail = false;
let flowHasMsisdn = false;
for (const stage of flow.stages) {
if (stage === EMAIL_STAGE_TYPE) {
flowHasEmail = true;
} else if (stage == MSISDN_STAGE_TYPE) {
flowHasMsisdn = true;
}
}
if (flowHasEmail == haveEmail && flowHasMsisdn == haveMsisdn) {
return flow;
}
}
// Throw an error with a fairly generic description, but with more
// information such that the app can give a better one if so desired.
const err = new Error("No appropriate authentication flow found");
err.name = 'NoAuthFlowFoundError';
err.required_stages = [];
if (haveEmail) err.required_stages.push(EMAIL_STAGE_TYPE);
if (haveMsisdn) err.required_stages.push(MSISDN_STAGE_TYPE);
err.available_flows = flows;
throw err;
},
/**
* Get the first uncompleted stage in the given flow
*
* @private
* @param {object} flow
* @return {string} login type
*/
_firstUncompletedStage: function(flow) {
const completed = (this._data || {}).completed || [];
for (let i = 0; i < flow.stages.length; ++i) {
const stageType = flow.stages[i];
if (completed.indexOf(stageType) === -1) {
return stageType;
}
}
},
};
/** */
module.exports = InteractiveAuth;
+11 -7
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -21,9 +22,12 @@ module.exports.MatrixEvent = require("./models/event").MatrixEvent;
module.exports.EventStatus = require("./models/event").EventStatus;
/** The {@link module:store/memory.MatrixInMemoryStore|MatrixInMemoryStore} class. */
module.exports.MatrixInMemoryStore = require("./store/memory").MatrixInMemoryStore;
/** The {@link module:store/webstorage~WebStorageStore|WebStorageStore} class.
* <strong>Work in progress; unstable.</strong> */
module.exports.WebStorageStore = require("./store/webstorage");
/** The {@link module:store/indexeddb.IndexedDBStore|IndexedDBStore} class. */
module.exports.IndexedDBStore = require("./store/indexeddb").IndexedDBStore;
/** The {@link module:store/indexeddb.IndexedDBStoreBackend|IndexedDBStoreBackend} class. */
module.exports.IndexedDBStoreBackend = require("./store/indexeddb").IndexedDBStoreBackend;
/** The {@link module:sync-accumulator.SyncAccumulator|SyncAccumulator} class. */
module.exports.SyncAccumulator = require("./sync-accumulator");
/** The {@link module:http-api.MatrixHttpApi|MatrixHttpApi} class. */
module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi;
/** The {@link module:http-api.MatrixError|MatrixError} class. */
@@ -71,7 +75,7 @@ module.exports.createNewMatrixCall = require("./webrtc/call").createNewMatrixCal
// expose the underlying request object so different environments can use
// different request libs (e.g. request or browser-request)
var request;
let request;
/**
* The function used to perform HTTP requests. Only use this if you want to
* use a different HTTP library, e.g. Angular's <code>$http</code>. This should
@@ -97,7 +101,7 @@ module.exports.getRequest = function() {
* @param {requestWrapperFunction} wrapper The wrapping function.
*/
module.exports.wrapRequest = function(wrapper) {
var origRequest = request;
const origRequest = request;
request = function(options, callback) {
return wrapper(origRequest, options, callback);
};
@@ -122,12 +126,12 @@ module.exports.wrapRequest = function(wrapper) {
module.exports.createClient = function(opts) {
if (typeof opts === "string") {
opts = {
"baseUrl": opts
"baseUrl": opts,
};
}
opts.request = opts.request || request;
opts.store = opts.store || new module.exports.MatrixInMemoryStore({
localStorage: global.localStorage
localStorage: global.localStorage,
});
opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler();
return new module.exports.MatrixClient(opts);
@@ -17,18 +17,19 @@ limitations under the License.
/**
* @module models/event-timeline-set
*/
var EventEmitter = require("events").EventEmitter;
var utils = require("../utils");
var EventTimeline = require("./event-timeline");
const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
const EventTimeline = require("./event-timeline");
// var DEBUG = false;
var DEBUG = true;
const DEBUG = true;
let debuglog;
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
var debuglog = console.log.bind(console);
debuglog = console.log.bind(console);
} else {
var debuglog = function() {};
debuglog = function() {};
}
/**
@@ -105,8 +106,7 @@ EventTimelineSet.prototype.getPendingEvents = function() {
if (this._filter) {
return this._filter.filterRoomTimeline(this.room.getPendingEvents());
}
else {
} else {
return this.room.getPendingEvents();
}
};
@@ -136,7 +136,7 @@ EventTimelineSet.prototype.eventIdToTimeline = function(eventId) {
* @param {String} newEventId event ID of the replacement event
*/
EventTimelineSet.prototype.replaceEventId = function(oldEventId, newEventId) {
var existingTimeline = this._eventIdToTimeline[oldEventId];
const existingTimeline = this._eventIdToTimeline[oldEventId];
if (existingTimeline) {
delete this._eventIdToTimeline[oldEventId];
this._eventIdToTimeline[newEventId] = existingTimeline;
@@ -154,10 +154,11 @@ EventTimelineSet.prototype.replaceEventId = function(oldEventId, newEventId) {
* @fires module:client~MatrixClient#event:"Room.timelineReset"
*/
EventTimelineSet.prototype.resetLiveTimeline = function(backPaginationToken, flush) {
var newTimeline;
// if timeline support is disabled, forget about the old timelines
const resetAllTimelines = !this._timelineSupport || flush;
if (!this._timelineSupport || flush) {
// if timeline support is disabled, forget about the old timelines
let newTimeline;
if (resetAllTimelines) {
newTimeline = new EventTimeline(this);
this._timelines = [newTimeline];
this._eventIdToTimeline = {};
@@ -166,12 +167,16 @@ EventTimelineSet.prototype.resetLiveTimeline = function(backPaginationToken, flu
}
// 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; }
const evMap = this._liveTimeline.getState(EventTimeline.FORWARDS).events;
const events = [];
for (const evtype in evMap) {
if (!evMap.hasOwnProperty(evtype)) {
continue;
}
for (const stateKey in evMap[evtype]) {
if (!evMap[evtype].hasOwnProperty(stateKey)) {
continue;
}
events.push(evMap[evtype][stateKey]);
}
}
@@ -183,7 +188,7 @@ EventTimelineSet.prototype.resetLiveTimeline = function(backPaginationToken, flu
newTimeline.setPaginationToken(backPaginationToken, EventTimeline.BACKWARDS);
this._liveTimeline = newTimeline;
this.emit("Room.timelineReset", this.room, this);
this.emit("Room.timelineReset", this.room, this, resetAllTimelines);
};
/**
@@ -194,7 +199,7 @@ EventTimelineSet.prototype.resetLiveTimeline = function(backPaginationToken, flu
* the given event, or null if unknown
*/
EventTimelineSet.prototype.getTimelineForEvent = function(eventId) {
var res = this._eventIdToTimeline[eventId];
const res = this._eventIdToTimeline[eventId];
return (res === undefined) ? null : res;
};
@@ -205,12 +210,13 @@ EventTimelineSet.prototype.getTimelineForEvent = function(eventId) {
* @return {?module:models/event~MatrixEvent} the given event, or undefined if unknown
*/
EventTimelineSet.prototype.findEventById = function(eventId) {
var tl = this.getTimelineForEvent(eventId);
const tl = this.getTimelineForEvent(eventId);
if (!tl) {
return undefined;
}
return utils.findElement(tl.getEvents(),
function(ev) { return ev.getId() == eventId; });
return utils.findElement(tl.getEvents(), function(ev) {
return ev.getId() == eventId;
});
};
/**
@@ -225,7 +231,7 @@ EventTimelineSet.prototype.addTimeline = function() {
" it.");
}
var timeline = new EventTimeline(this);
const timeline = new EventTimeline(this);
this._timelines.push(timeline);
return timeline;
};
@@ -254,14 +260,14 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
timeline, paginationToken) {
if (!timeline) {
throw new Error(
"'timeline' not specified for EventTimelineSet.addEventsToTimeline"
"'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"
"the live timeline - use Room.addLiveEvents instead",
);
}
@@ -272,9 +278,9 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
}
}
var direction = toStartOfTimeline ? EventTimeline.BACKWARDS :
const direction = toStartOfTimeline ? EventTimeline.BACKWARDS :
EventTimeline.FORWARDS;
var inverseDirection = toStartOfTimeline ? EventTimeline.FORWARDS :
const inverseDirection = toStartOfTimeline ? EventTimeline.FORWARDS :
EventTimeline.BACKWARDS;
// Adding events to timelines can be quite complicated. The following
@@ -346,13 +352,13 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
// 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();
let didUpdate = false;
let lastEventWasNew = false;
for (let i = 0; i < events.length; i++) {
const event = events[i];
const eventId = event.getId();
var existingTimeline = this._eventIdToTimeline[eventId];
const existingTimeline = this._eventIdToTimeline[eventId];
if (!existingTimeline) {
// we don't know about this event yet. Just add it to the timeline.
@@ -369,7 +375,7 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
continue;
}
var neighbour = timeline.getNeighbouringTimeline(direction);
const 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
@@ -417,25 +423,25 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
*/
EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
if (this._filter) {
var events = this._filter.filterRoomTimeline([event]);
const events = this._filter.filterRoomTimeline([event]);
if (!events.length) {
return;
}
}
var timeline = this._eventIdToTimeline[event.getId()];
const 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++) {
const tlEvents = timeline.getEvents();
for (let 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
false,
);
if (!tlEvents[j].encryptedType) {
@@ -470,11 +476,11 @@ EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
*/
EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
toStartOfTimeline) {
var eventId = event.getId();
const eventId = event.getId();
timeline.addEvent(event, toStartOfTimeline);
this._eventIdToTimeline[eventId] = timeline;
var data = {
const data = {
timeline: timeline,
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
};
@@ -495,7 +501,7 @@ EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
EventTimelineSet.prototype.handleRemoteEcho = function(localEvent, oldEventId,
newEventId) {
// XXX: why don't we infer newEventId from localEvent?
var existingTimeline = this._eventIdToTimeline[oldEventId];
const existingTimeline = this._eventIdToTimeline[oldEventId];
if (existingTimeline) {
delete this._eventIdToTimeline[oldEventId];
this._eventIdToTimeline[newEventId] = existingTimeline;
@@ -504,8 +510,7 @@ EventTimelineSet.prototype.handleRemoteEcho = function(localEvent, oldEventId,
if (this._filter.filterRoomTimeline([localEvent]).length) {
this.addEventToTimeline(localEvent, this._liveTimeline, false);
}
}
else {
} else {
this.addEventToTimeline(localEvent, this._liveTimeline, false);
}
}
@@ -520,15 +525,15 @@ EventTimelineSet.prototype.handleRemoteEcho = function(localEvent, oldEventId,
* in this room.
*/
EventTimelineSet.prototype.removeEvent = function(eventId) {
var timeline = this._eventIdToTimeline[eventId];
const timeline = this._eventIdToTimeline[eventId];
if (!timeline) {
return null;
}
var removed = timeline.removeEvent(eventId);
const removed = timeline.removeEvent(eventId);
if (removed) {
delete this._eventIdToTimeline[eventId];
var data = {
const data = {
timeline: timeline,
};
this.emit("Room.timeline", removed, this.room, undefined, true, data);
@@ -554,8 +559,8 @@ EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
return 0;
}
var timeline1 = this._eventIdToTimeline[eventId1];
var timeline2 = this._eventIdToTimeline[eventId2];
const timeline1 = this._eventIdToTimeline[eventId1];
const timeline2 = this._eventIdToTimeline[eventId2];
if (timeline1 === undefined) {
return null;
@@ -567,11 +572,11 @@ EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
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 &&
let idx1, idx2;
const events = timeline1.getEvents();
for (let idx = 0; idx < events.length &&
(idx1 === undefined || idx2 === undefined); idx++) {
var evId = events[idx].getId();
const evId = events[idx].getId();
if (evId == eventId1) {
idx1 = idx;
}
@@ -586,7 +591,7 @@ EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
// linkedlist to see which comes first.
// first work forwards from timeline1
var tl = timeline1;
let tl = timeline1;
while (tl) {
if (tl === timeline2) {
// timeline1 is before timeline2
@@ -651,4 +656,5 @@ module.exports = EventTimelineSet;
* @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
* @param {boolean} resetAllTimelines True if all timelines were reset.
*/
@@ -1,12 +1,27 @@
/*
Copyright 2016, 2017 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
*/
var RoomState = require("./room-state");
var utils = require("../utils");
var MatrixEvent = require("./event").MatrixEvent;
const RoomState = require("./room-state");
const utils = require("../utils");
const MatrixEvent = require("./event").MatrixEvent;
/**
* Construct a new EventTimeline
@@ -75,10 +90,15 @@ EventTimeline.prototype.initialiseState = function(stateEvents) {
// we deep-copy the events here, in case they get changed later - we don't
// want changes to the start state leaking through to the end state.
var oldStateEvents = utils.map(
const oldStateEvents = utils.map(
utils.deepCopy(
stateEvents.map(function(mxEvent) { return mxEvent.event; })
), function(ev) { return new MatrixEvent(ev); });
stateEvents.map(function(mxEvent) {
return mxEvent.event;
}),
),
function(ev) {
return new MatrixEvent(ev);
});
this._startState.setStateEvents(oldStateEvents);
this._endState.setStateEvents(stateEvents);
@@ -232,13 +252,12 @@ EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction)
* @param {boolean} atStart true to insert new event at the start
*/
EventTimeline.prototype.addEvent = function(event, atStart) {
var stateContext = atStart ? this._startState : this._endState;
const stateContext = atStart ? this._startState : this._endState;
// only call setEventMetadata on the unfiltered timelineSets
var timelineSet = this.getTimelineSet();
const timelineSet = this.getTimelineSet();
if (timelineSet.room &&
timelineSet.room.getUnfilteredTimelineSet() === timelineSet)
{
timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
EventTimeline.setEventMetadata(event, stateContext, atStart);
// modify state
@@ -260,7 +279,7 @@ EventTimeline.prototype.addEvent = function(event, atStart) {
}
}
var insertIndex;
let insertIndex;
if (atStart) {
insertIndex = 0;
@@ -284,11 +303,11 @@ EventTimeline.prototype.addEvent = function(event, atStart) {
EventTimeline.setEventMetadata = function(event, stateContext, toStartOfTimeline) {
// set sender and target properties
event.sender = stateContext.getSentinelMember(
event.getSender()
event.getSender(),
);
if (event.getType() === "m.room.member") {
event.target = stateContext.getSentinelMember(
event.getStateKey()
event.getStateKey(),
);
}
if (event.isState()) {
@@ -309,8 +328,8 @@ EventTimeline.setEventMetadata = function(event, stateContext, toStartOfTimeline
* @return {?MatrixEvent} removed event, or null if not found
*/
EventTimeline.prototype.removeEvent = function(eventId) {
for (var i = this._events.length - 1; i >= 0; i--) {
var ev = this._events[i];
for (let i = this._events.length - 1; i >= 0; i--) {
const ev = this._events[i];
if (ev.getId() == eventId) {
this._events.splice(i, 1);
if (i < this._baseIndex) {
+77 -35
View File
@@ -21,9 +21,9 @@ limitations under the License.
* @module models/event
*/
var EventEmitter = require("events").EventEmitter;
const EventEmitter = require("events").EventEmitter;
var utils = require('../utils.js');
const utils = require('../utils.js');
/**
* Enum for event statuses.
@@ -49,6 +49,8 @@ module.exports.EventStatus = {
CANCELLED: "cancelled",
};
const interns = {};
/**
* Construct a Matrix Event object
* @constructor
@@ -61,23 +63,53 @@ module.exports.EventStatus = {
* 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.
* this is a presence event. This is only guaranteed to be set for events that
* appear in a timeline, ie. do not guarantee that it will be set on state
* events.
* @prop {RoomMember} target The room member who is the target of this event, e.g.
* the invitee, the person being banned, etc.
* @prop {EventStatus} status The sending status of the event.
* @prop {Error} error most recent error associated with sending the event, if any
* @prop {boolean} forwardLooking True if this event is 'forward looking', meaning
* that getDirectionalContent() will return event.content and not event.prev_content.
* Default: true. <strong>This property is experimental and may change.</strong>
*/
module.exports.MatrixEvent = function MatrixEvent(
event
event,
) {
// intern the values of matrix events to force share strings and reduce the
// amount of needless string duplication. This can save moderate amounts of
// memory (~10% on a 350MB heap).
["state_key", "type", "sender", "room_id"].forEach((prop) => {
if (!event[prop]) {
return;
}
if (!interns[event[prop]]) {
interns[event[prop]] = event[prop];
}
event[prop] = interns[event[prop]];
});
["membership", "avatar_url", "displayname"].forEach((prop) => {
if (!event.content || !event.content[prop]) {
return;
}
if (!interns[event.content[prop]]) {
interns[event.content[prop]] = event.content[prop];
}
event.content[prop] = interns[event.content[prop]];
});
this.event = event || {};
this.sender = null;
this.target = null;
this.status = null;
this.error = null;
this.forwardLooking = true;
this._pushActions = null;
this._date = this.event.origin_server_ts ?
new Date(this.event.origin_server_ts) : null;
this._clearEvent = {};
this._keysProved = {};
@@ -142,6 +174,14 @@ utils.extend(module.exports.MatrixEvent.prototype, {
return this.event.origin_server_ts;
},
/**
* Get the timestamp of this event, as a Date object.
* @return {Date} The event date, e.g. <code>new Date(1433502692297)</code>
*/
getDate: function() {
return this._date;
},
/**
* Get the (decrypted, if necessary) event content JSON.
*
@@ -330,18 +370,22 @@ utils.extend(module.exports.MatrixEvent.prototype, {
}
this.event.unsigned.redacted_because = redaction_event.event;
var key;
let key;
for (key in this.event) {
if (!this.event.hasOwnProperty(key)) { continue; }
if (!this.event.hasOwnProperty(key)) {
continue;
}
if (!_REDACT_KEEP_KEY_MAP[key]) {
delete this.event[key];
}
}
var keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {};
var content = this.getContent();
const keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {};
const content = this.getContent();
for (key in content) {
if (!content.hasOwnProperty(key)) { continue; }
if (!content.hasOwnProperty(key)) {
continue;
}
if (!keeps[key]) {
delete content[key];
}
@@ -374,38 +418,38 @@ utils.extend(module.exports.MatrixEvent.prototype, {
setPushActions: function(pushActions) {
this._pushActions = pushActions;
},
/**
* Replace the `event` property and recalculate any properties based on it.
* @param {Object} event the object to assign to the `event` property
*/
handleRemoteEcho: function(event) {
this.event = event;
// successfully sent.
this.status = null;
this._date = new Date(this.event.origin_server_ts);
},
});
/* http://matrix.org/docs/spec/r0.0.1/client_server.html#redactions says:
/* _REDACT_KEEP_KEY_MAP gives the keys we keep when an event is redacted
*
* the server should strip off any keys not in the following list:
* event_id
* type
* room_id
* user_id
* state_key
* prev_state
* content
* [we keep 'unsigned' as well, since that is created by the local server]
* This is specified here:
* http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#redactions
*
* The content object should also be stripped of all keys, unless it is one of
* one of the following event types:
* m.room.member allows key membership
* m.room.create allows key creator
* m.room.join_rules allows key join_rule
* m.room.power_levels allows keys ban, events, events_default, kick,
* redact, state_default, users, users_default.
* m.room.aliases allows key aliases
* Also:
* - We keep 'unsigned' since that is created by the local server
* - We keep user_id for backwards-compat with v1
*/
// a map giving the keys we keep when an event is redacted
var _REDACT_KEEP_KEY_MAP = [
'event_id', 'type', 'room_id', 'user_id', 'state_key', 'prev_state',
'content', 'unsigned',
].reduce(function(ret, val) { ret[val] = 1; return ret; }, {});
const _REDACT_KEEP_KEY_MAP = [
'event_id', 'type', 'room_id', 'user_id', 'sender', 'state_key', 'prev_state',
'content', 'unsigned', 'origin_server_ts',
].reduce(function(ret, val) {
ret[val] = 1; return ret;
}, {});
// a map from event type to the .content keys we keep when an event is redacted
var _REDACT_KEEP_CONTENT_MAP = {
const _REDACT_KEEP_CONTENT_MAP = {
'm.room.member': {'membership': 1},
'm.room.create': {'creator': 1},
'm.room.join_rules': {'join_rule': 1},
@@ -417,8 +461,6 @@ var _REDACT_KEEP_CONTENT_MAP = {
};
/**
* Fires when an event is decrypted
*
@@ -17,10 +17,10 @@ limitations under the License.
/**
* @module models/room-member
*/
var EventEmitter = require("events").EventEmitter;
var ContentRepo = require("../content-repo");
const EventEmitter = require("events").EventEmitter;
const ContentRepo = require("../content-repo");
var utils = require("../utils");
const utils = require("../utils");
/**
* Construct a new room member.
@@ -52,7 +52,7 @@ function RoomMember(roomId, userId) {
this.user = null;
this.membership = null;
this.events = {
member: null
member: null,
};
this._updateModifiedTime();
}
@@ -73,10 +73,10 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) {
}
this.events.member = event;
var oldMembership = this.membership;
const oldMembership = this.membership;
this.membership = event.getDirectionalContent().membership;
var oldName = this.name;
const oldName = this.name;
this.name = calculateDisplayName(this, event, roomState);
if (oldMembership !== this.membership) {
this._updateModifiedTime();
@@ -99,12 +99,12 @@ RoomMember.prototype.setPowerLevelEvent = function(powerLevelEvent) {
if (powerLevelEvent.getType() !== "m.room.power_levels") {
return;
}
var maxLevel = powerLevelEvent.getContent().users_default || 0;
let maxLevel = powerLevelEvent.getContent().users_default || 0;
utils.forEach(utils.values(powerLevelEvent.getContent().users), function(lvl) {
maxLevel = Math.max(maxLevel, lvl);
});
var oldPowerLevel = this.powerLevel;
var oldPowerLevelNorm = this.powerLevelNorm;
const oldPowerLevel = this.powerLevel;
const oldPowerLevelNorm = this.powerLevelNorm;
if (powerLevelEvent.getContent().users[this.userId] !== undefined) {
this.powerLevel = powerLevelEvent.getContent().users[this.userId];
@@ -136,9 +136,9 @@ RoomMember.prototype.setTypingEvent = function(event) {
if (event.getType() !== "m.typing") {
return;
}
var oldTyping = this.typing;
const oldTyping = this.typing;
this.typing = false;
var typingList = event.getContent().user_ids;
const typingList = event.getContent().user_ids;
if (!utils.isArray(typingList)) {
// malformed event :/ bail early. TODO: whine?
return;
@@ -189,28 +189,29 @@ RoomMember.prototype.getLastModifiedTime = function() {
*/
RoomMember.prototype.getAvatarUrl =
function(baseUrl, width, height, resizeMethod, allowDefault, allowDirectLinks) {
if (allowDefault === undefined) { allowDefault = true; }
if (allowDefault === undefined) {
allowDefault = true;
}
if (!this.events.member && !allowDefault) {
return null;
}
var rawUrl = this.events.member ? this.events.member.getContent().avatar_url : null;
var httpUrl = ContentRepo.getHttpUriForMxc(
baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks
const rawUrl = this.events.member ? this.events.member.getContent().avatar_url : null;
const httpUrl = ContentRepo.getHttpUriForMxc(
baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks,
);
if (httpUrl) {
return httpUrl;
}
else if (allowDefault) {
} else if (allowDefault) {
return ContentRepo.getIdenticonUri(
baseUrl, this.userId, width, height
baseUrl, this.userId, width, height,
);
}
return null;
};
function calculateDisplayName(member, event, roomState) {
var displayName = event.getDirectionalContent().displayname;
var selfUserId = member.userId;
const displayName = event.getDirectionalContent().displayname;
const selfUserId = member.userId;
if (!displayName) {
return selfUserId;
@@ -220,8 +221,8 @@ function calculateDisplayName(member, event, roomState) {
return displayName;
}
var userIds = roomState.getUserIdsWithDisplayName(displayName);
var otherUsers = userIds.filter(function(u) {
const userIds = roomState.getUserIdsWithDisplayName(displayName);
const otherUsers = userIds.filter(function(u) {
return u !== selfUserId;
});
if (otherUsers.length > 0) {
@@ -17,10 +17,10 @@ limitations under the License.
/**
* @module models/room-state
*/
var EventEmitter = require("events").EventEmitter;
const EventEmitter = require("events").EventEmitter;
var utils = require("../utils");
var RoomMember = require("./room-member");
const utils = require("../utils");
const RoomMember = require("./room-member");
/**
* Construct room state.
@@ -100,7 +100,7 @@ RoomState.prototype.getStateEvents = function(eventType, stateKey) {
if (stateKey === undefined) { // return all values
return utils.values(this.events[eventType]);
}
var event = this.events[eventType][stateKey];
const event = this.events[eventType][stateKey];
return event ? event : null;
};
@@ -115,13 +115,17 @@ RoomState.prototype.getStateEvents = function(eventType, stateKey) {
* @fires module:client~MatrixClient#event:"RoomState.events"
*/
RoomState.prototype.setStateEvents = function(stateEvents) {
var self = this;
const self = this;
this._updateModifiedTime();
// update the core event dict
utils.forEach(stateEvents, function(event) {
if (event.getRoomId() !== self.roomId) { return; }
if (!event.isState()) { return; }
if (event.getRoomId() !== self.roomId) {
return;
}
if (!event.isState()) {
return;
}
if (self.events[event.getType()] === undefined) {
self.events[event.getType()] = {};
@@ -129,7 +133,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
self.events[event.getType()][event.getStateKey()] = event;
if (event.getType() === "m.room.member") {
_updateDisplayNameCache(
self, event.getStateKey(), event.getContent().displayname
self, event.getStateKey(), event.getContent().displayname,
);
_updateThirdPartyTokenCache(self, event);
}
@@ -141,18 +145,21 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
// the given array (e.g. disambiguating display names in one go to do both
// clashing names rather than progressively which only catches 1 of them).
utils.forEach(stateEvents, function(event) {
if (event.getRoomId() !== self.roomId) { return; }
if (!event.isState()) { return; }
if (event.getRoomId() !== self.roomId) {
return;
}
if (!event.isState()) {
return;
}
if (event.getType() === "m.room.member") {
var userId = event.getStateKey();
const userId = event.getStateKey();
// leave events apparently elide the displayname or avatar_url,
// so let's fake one up so that we don't leak user ids
// into the timeline
if (event.getContent().membership === "leave" ||
event.getContent().membership === "ban")
{
event.getContent().membership === "ban") {
event.getContent().avatar_url =
event.getContent().avatar_url ||
event.getPrevContent().avatar_url;
@@ -161,7 +168,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
event.getPrevContent().displayname;
}
var member = self.members[userId];
let member = self.members[userId];
if (!member) {
member = new RoomMember(event.getRoomId(), userId);
self.emit("RoomState.newMember", event, self, member);
@@ -171,11 +178,11 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
// so we don't make assumptions about the properties of RoomMember
// (e.g. and manage to break it because deep copying doesn't do
// everything).
var sentinel = new RoomMember(event.getRoomId(), userId);
const sentinel = new RoomMember(event.getRoomId(), userId);
utils.forEach([member, sentinel], function(roomMember) {
roomMember.setMembershipEvent(event, self);
// this member may have a power level already, so set it.
var pwrLvlEvent = self.getStateEvents("m.room.power_levels", "");
const pwrLvlEvent = self.getStateEvents("m.room.power_levels", "");
if (pwrLvlEvent) {
roomMember.setPowerLevelEvent(pwrLvlEvent);
}
@@ -184,9 +191,8 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
self._sentinels[userId] = sentinel;
self.members[userId] = member;
self.emit("RoomState.members", event, self, member);
}
else if (event.getType() === "m.room.power_levels") {
var members = utils.values(self.members);
} else if (event.getType() === "m.room.power_levels") {
const members = utils.values(self.members);
utils.forEach(members, function(member) {
member.setPowerLevelEvent(event);
self.emit("RoomState.members", event, self, member);
@@ -253,7 +259,7 @@ RoomState.prototype.maySendMessage = function(userId) {
/**
* Returns true if the given user ID has permission to send a normal
* event of type `eventType` into this room.
* @param {string} type The type of event to test
* @param {string} eventType The type of event to test
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* the given type of event into this room,
@@ -267,8 +273,8 @@ RoomState.prototype.maySendEvent = function(eventType, userId) {
/**
* Returns true if the given MatrixClient has permission to send a state
* event of type `stateEventType` into this room.
* @param {string} type The type of state events to test
* @param {MatrixClient} The client to test permission for
* @param {string} stateEventType The type of state events to test
* @param {MatrixClient} cli The client to test permission for
* @return {boolean} true if the given client should be permitted to send
* the given type of state event into this room,
* according to the room's state.
@@ -283,7 +289,7 @@ RoomState.prototype.mayClientSendStateEvent = function(stateEventType, cli) {
/**
* Returns true if the given user ID has permission to send a state
* event of type `stateEventType` into this room.
* @param {string} type The type of state events to test
* @param {string} stateEventType The type of state events to test
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* the given type of state event into this room,
@@ -296,7 +302,7 @@ RoomState.prototype.maySendStateEvent = function(stateEventType, userId) {
/**
* Returns true if the given user ID has permission to send a normal or state
* event of type `eventType` into this room.
* @param {string} type The type of event to test
* @param {string} eventType The type of event to test
* @param {string} userId The user ID of the user to test permission for
* @param {boolean} state If true, tests if the user may send a state
event of this type. Otherwise tests whether
@@ -306,26 +312,22 @@ RoomState.prototype.maySendStateEvent = function(stateEventType, userId) {
* according to the room's state.
*/
RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
var member = this.getMember(userId);
if (!member || member.membership == 'leave') { return false; }
const member = this.getMember(userId);
if (!member || member.membership == 'leave') {
return false;
}
var power_levels_event = this.getStateEvents('m.room.power_levels', '');
const power_levels_event = this.getStateEvents('m.room.power_levels', '');
var power_levels;
var events_levels = {};
let power_levels;
let events_levels = {};
var default_user_level = 0;
var user_levels = [];
var state_default = 0;
var events_default = 0;
let state_default = 0;
let events_default = 0;
if (power_levels_event) {
power_levels = power_levels_event.getContent();
events_levels = power_levels.events || {};
default_user_level = parseInt(power_levels.users_default || 0);
user_levels = power_levels.users || {};
if (power_levels.state_default !== undefined) {
state_default = power_levels.state_default;
} else {
@@ -336,7 +338,7 @@ RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
}
}
var required_level = state ? state_default : events_default;
let required_level = state ? state_default : events_default;
if (events_levels[eventType] !== undefined) {
required_level = events_levels[eventType];
}
@@ -353,12 +355,12 @@ function _updateThirdPartyTokenCache(roomState, memberEvent) {
if (!memberEvent.getContent().third_party_invite) {
return;
}
var token = (memberEvent.getContent().third_party_invite.signed || {}).token;
const token = (memberEvent.getContent().third_party_invite.signed || {}).token;
if (!token) {
return;
}
var threePidInvite = roomState.getStateEvents(
"m.room.third_party_invite", token
const threePidInvite = roomState.getStateEvents(
"m.room.third_party_invite", token,
);
if (!threePidInvite) {
return;
@@ -367,15 +369,15 @@ function _updateThirdPartyTokenCache(roomState, memberEvent) {
}
function _updateDisplayNameCache(roomState, userId, displayName) {
var oldName = roomState._userIdsToDisplayNames[userId];
const oldName = roomState._userIdsToDisplayNames[userId];
delete roomState._userIdsToDisplayNames[userId];
if (oldName) {
// Remove the old name from the cache.
// We clobber the user_id > name lookup but the name -> [user_id] lookup
// means we need to remove that user ID from that array rather than nuking
// the lot.
var existingUserIds = roomState._displayNameToUserIds[oldName] || [];
for (var i = 0; i < existingUserIds.length; i++) {
const existingUserIds = roomState._displayNameToUserIds[oldName] || [];
for (let i = 0; i < existingUserIds.length; i++) {
if (existingUserIds[i] === userId) {
// remove this user ID from this array
existingUserIds.splice(i, 1);
+144 -130
View File
@@ -17,30 +17,30 @@ limitations under the License.
/**
* @module models/room
*/
var EventEmitter = require("events").EventEmitter;
const EventEmitter = require("events").EventEmitter;
var EventStatus = require("./event").EventStatus;
var RoomSummary = require("./room-summary");
var MatrixEvent = require("./event").MatrixEvent;
var utils = require("../utils");
var ContentRepo = require("../content-repo");
var EventTimeline = require("./event-timeline");
var EventTimelineSet = require("./event-timeline-set");
const EventStatus = require("./event").EventStatus;
const RoomSummary = require("./room-summary");
const MatrixEvent = require("./event").MatrixEvent;
const utils = require("../utils");
const ContentRepo = require("../content-repo");
const EventTimeline = require("./event-timeline");
const EventTimelineSet = require("./event-timeline-set");
function synthesizeReceipt(userId, event, receiptType) {
// console.log("synthesizing receipt for "+event.getId());
// This is really ugly because JS has no way to express an object literal
// where the name of a key comes from an expression
var fakeReceipt = {
const fakeReceipt = {
content: {},
type: "m.receipt",
room_id: event.getRoomId()
room_id: event.getRoomId(),
};
fakeReceipt.content[event.getId()] = {};
fakeReceipt.content[event.getId()][receiptType] = {};
fakeReceipt.content[event.getId()][receiptType][userId] = {
ts: event.getTs()
ts: event.getTs(),
};
return new MatrixEvent(fakeReceipt);
}
@@ -108,7 +108,7 @@ function Room(roomId, opts) {
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
throw new Error(
"opts.pendingEventOrdering MUST be either 'chronological' or " +
"'detached'. Got: '" + opts.pendingEventOrdering + "'"
"'detached'. Got: '" + opts.pendingEventOrdering + "'",
);
}
@@ -165,6 +165,8 @@ function Room(roomId, opts) {
if (this._opts.pendingEventOrdering == "detached") {
this._pendingEventList = [];
}
this._blacklistUnverifiedDevices = false; // read by megolm
}
utils.inherits(Room, EventEmitter);
@@ -202,10 +204,12 @@ Room.prototype.getLiveTimeline = function() {
* <p>This is used when /sync returns a 'limited' timeline.
*
* @param {string=} backPaginationToken token for back-paginating the new timeline
* @param {boolean=} flush True to remove all events in all timelines. If false, only
* the live timeline is reset.
*/
Room.prototype.resetLiveTimeline = function(backPaginationToken) {
for (var i = 0; i < this._timelineSets.length; i++) {
this._timelineSets[i].resetLiveTimeline(backPaginationToken);
Room.prototype.resetLiveTimeline = function(backPaginationToken, flush) {
for (let i = 0; i < this._timelineSets.length; i++) {
this._timelineSets[i].resetLiveTimeline(backPaginationToken, flush);
}
this._fixUpLegacyTimelineFields();
@@ -294,6 +298,24 @@ Room.prototype.setUnreadNotificationCount = function(type, count) {
this._notificationCounts[type] = count;
};
/**
* Whether to send encrypted messages to devices within this room.
* Will be ignored if MatrixClient's blacklistUnverifiedDevices setting is true.
* @param {boolean} value if true, blacklist unverified devices.
*/
Room.prototype.setBlacklistUnverifiedDevices = function(value) {
this._blacklistUnverifiedDevices = value;
};
/**
* Whether to send encrypted messages to devices within this room.
* Will be ignored if MatrixClient's blacklistUnverifiedDevices setting is true.
* @return {boolean} true if blacklisting unverified devices.
*/
Room.prototype.getBlacklistUnverifiedDevices = function() {
return this._blacklistUnverifiedDevices;
};
/**
* Get the avatar URL for a room if one was set.
* @param {String} baseUrl The homeserver base URL. See
@@ -308,21 +330,22 @@ Room.prototype.setUnreadNotificationCount = function(type, count) {
*/
Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
allowDefault) {
var roomAvatarEvent = this.currentState.getStateEvents("m.room.avatar", "");
if (allowDefault === undefined) { allowDefault = true; }
const roomAvatarEvent = this.currentState.getStateEvents("m.room.avatar", "");
if (allowDefault === undefined) {
allowDefault = true;
}
if (!roomAvatarEvent && !allowDefault) {
return null;
}
var mainUrl = roomAvatarEvent ? roomAvatarEvent.getContent().url : null;
const mainUrl = roomAvatarEvent ? roomAvatarEvent.getContent().url : null;
if (mainUrl) {
return ContentRepo.getHttpUriForMxc(
baseUrl, mainUrl, width, height, resizeMethod
baseUrl, mainUrl, width, height, resizeMethod,
);
}
else if (allowDefault) {
} else if (allowDefault) {
return ContentRepo.getIdenticonUri(
baseUrl, this.roomId, width, height
baseUrl, this.roomId, width, height,
);
}
@@ -336,15 +359,15 @@ Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
* @return {array} The room's alias as an array of strings
*/
Room.prototype.getAliases = function() {
var alias_strings = [];
const alias_strings = [];
var alias_events = this.currentState.getStateEvents("m.room.aliases");
const alias_events = this.currentState.getStateEvents("m.room.aliases");
if (alias_events) {
for (var i = 0; i < alias_events.length; ++i) {
var alias_event = alias_events[i];
for (let i = 0; i < alias_events.length; ++i) {
const alias_event = alias_events[i];
if (utils.isArray(alias_event.getContent().aliases)) {
Array.prototype.push.apply(
alias_strings, alias_event.getContent().aliases
alias_strings, alias_event.getContent().aliases,
);
}
}
@@ -359,7 +382,7 @@ Room.prototype.getAliases = function() {
* @return {?string} The room's canonical alias, or null if there is none
*/
Room.prototype.getCanonicalAlias = function() {
var canonicalAlias = this.currentState.getStateEvents("m.room.canonical_alias", "");
const canonicalAlias = this.currentState.getStateEvents("m.room.canonical_alias", "");
if (canonicalAlias) {
return canonicalAlias.getContent().alias;
}
@@ -389,7 +412,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
timeline, paginationToken) {
timeline.getTimelineSet().addEventsToTimeline(
events, toStartOfTimeline,
timeline, paginationToken
timeline, paginationToken,
);
};
@@ -399,7 +422,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
* @return {RoomMember} The member or <code>null</code>.
*/
Room.prototype.getMember = function(userId) {
var member = this.currentState.members[userId];
const member = this.currentState.members[userId];
if (!member) {
return null;
}
@@ -444,7 +467,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
* @return {boolean} True if this user_id has the given membership state.
*/
Room.prototype.hasMembershipState = function(userId, membership) {
var member = this.getMember(userId);
const member = this.getMember(userId);
if (!member) {
return false;
}
@@ -460,8 +483,8 @@ Room.prototype.getOrCreateFilteredTimelineSet = function(filter) {
if (this._filteredTimelineSets[filter.filterId]) {
return this._filteredTimelineSets[filter.filterId];
}
var opts = Object.assign({ filter: filter }, this._opts);
var timelineSet = new EventTimelineSet(this, opts);
const opts = Object.assign({ filter: filter }, this._opts);
const timelineSet = new EventTimelineSet(this, opts);
reEmit(this, timelineSet, ["Room.timeline", "Room.timelineReset"]);
this._filteredTimelineSets[filter.filterId] = timelineSet;
this._timelineSets.push(timelineSet);
@@ -473,21 +496,21 @@ Room.prototype.getOrCreateFilteredTimelineSet = function(filter) {
// may have grown huge and so take a long time to filter.
// see https://github.com/vector-im/vector-web/issues/2109
var unfilteredLiveTimeline = this.getLiveTimeline();
const unfilteredLiveTimeline = this.getLiveTimeline();
unfilteredLiveTimeline.getEvents().forEach(function(event) {
timelineSet.addLiveEvent(event);
});
// find the earliest unfiltered timeline
var timeline = unfilteredLiveTimeline;
let timeline = unfilteredLiveTimeline;
while (timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)) {
timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS);
}
timelineSet.getLiveTimeline().setPaginationToken(
timeline.getPaginationToken(EventTimeline.BACKWARDS),
EventTimeline.BACKWARDS
EventTimeline.BACKWARDS,
);
// alternatively, we could try to do something like this to try and re-paginate
@@ -507,9 +530,9 @@ Room.prototype.getOrCreateFilteredTimelineSet = function(filter) {
* @param {Filter} filter the filter whose timelineSet is to be forgotten
*/
Room.prototype.removeFilteredTimelineSet = function(filter) {
var timelineSet = this._filteredTimelineSets[filter.filterId];
const timelineSet = this._filteredTimelineSets[filter.filterId];
delete this._filteredTimelineSets[filter.filterId];
var i = this._timelineSets.indexOf(timelineSet);
const i = this._timelineSets.indexOf(timelineSet);
if (i > -1) {
this._timelineSets.splice(i, 1);
}
@@ -525,12 +548,12 @@ Room.prototype.removeFilteredTimelineSet = function(filter) {
* @private
*/
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
var i;
let i;
if (event.getType() === "m.room.redaction") {
var redactId = event.event.redacts;
const redactId = event.event.redacts;
// if we know about this event, redact its contents now.
var redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
if (redactedEvent) {
redactedEvent.makeRedacted(event);
this.emit("Room.redaction", event, this);
@@ -550,7 +573,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
}
if (event.getUnsigned().transaction_id) {
var existingEvent = this._txnToEvent[event.getUnsigned().transaction_id];
const existingEvent = this._txnToEvent[event.getUnsigned().transaction_id];
if (existingEvent) {
// remote echo of an event we sent earlier
this._handleRemoteEcho(event, existingEvent);
@@ -568,7 +591,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
// pointing to an event that wasn't yet in the timeline
if (event.sender) {
this.addReceipt(synthesizeReceipt(
event.sender.userId, event, "m.read"
event.sender.userId, event, "m.read",
), true);
// Any live events from a user could be taken as implicit
@@ -615,7 +638,7 @@ Room.prototype.addPendingEvent = function(event, txnId) {
EventTimeline.setEventMetadata(
event,
this.getLiveTimeline().getState(EventTimeline.FORWARDS),
false
false,
);
this._txnToEvent[txnId] = event;
@@ -623,15 +646,14 @@ Room.prototype.addPendingEvent = function(event, txnId) {
if (this._opts.pendingEventOrdering == "detached") {
this._pendingEventList.push(event);
} else {
for (var i = 0; i < this._timelineSets.length; i++) {
var timelineSet = this._timelineSets[i];
for (let i = 0; i < this._timelineSets.length; i++) {
const timelineSet = this._timelineSets[i];
if (timelineSet.getFilter()) {
if (this._filter.filterRoomTimeline([event]).length) {
timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false);
}
}
else {
} else {
timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false);
}
@@ -656,9 +678,9 @@ Room.prototype.addPendingEvent = function(event, txnId) {
* @private
*/
Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
var oldEventId = localEvent.getId();
var newEventId = remoteEvent.getId();
var oldStatus = localEvent.status;
const oldEventId = localEvent.getId();
const newEventId = remoteEvent.getId();
const oldStatus = localEvent.status;
// no longer pending
delete this._txnToEvent[remoteEvent.transaction_id];
@@ -667,20 +689,18 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
if (this._pendingEventList) {
utils.removeElement(
this._pendingEventList,
function(ev) { return ev.getId() == oldEventId; },
false
function(ev) {
return ev.getId() == oldEventId;
}, false,
);
}
// replace the event source (this will preserve the plaintext payload if
// any, which is good, because we don't want to try decoding it again).
localEvent.event = remoteEvent.event;
localEvent.handleRemoteEcho(remoteEvent.event);
// successfully sent.
localEvent.status = null;
for (var i = 0; i < this._timelineSets.length; i++) {
var timelineSet = this._timelineSets[i];
for (let i = 0; i < this._timelineSets.length; i++) {
const timelineSet = this._timelineSets[i];
// if it's already in the timeline, update the timeline map. If it's not, add it.
timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
@@ -692,7 +712,7 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
/* a map from current event status to a list of allowed next statuses
*/
var ALLOWED_TRANSITIONS = {};
const ALLOWED_TRANSITIONS = {};
ALLOWED_TRANSITIONS[EventStatus.ENCRYPTING] = [
EventStatus.SENDING,
@@ -739,7 +759,7 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
// SENT races against /sync, so we have to special-case it.
if (newStatus == EventStatus.SENT) {
var timeline = this.getUnfilteredTimelineSet().eventIdToTimeline(newEventId);
const timeline = this.getUnfilteredTimelineSet().eventIdToTimeline(newEventId);
if (timeline) {
// we've already received the event via the event stream.
// nothing more to do here.
@@ -747,15 +767,15 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
}
}
var oldStatus = event.status;
var oldEventId = event.getId();
const oldStatus = event.status;
const oldEventId = event.getId();
if (!oldStatus) {
throw new Error("updatePendingEventStatus called on an event which is " +
"not a local echo.");
}
var allowed = ALLOWED_TRANSITIONS[oldStatus];
const allowed = ALLOWED_TRANSITIONS[oldStatus];
if (!allowed || allowed.indexOf(newStatus) < 0) {
throw new Error("Invalid EventStatus transition " + oldStatus + "->" +
newStatus);
@@ -770,17 +790,17 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
// if the event was already in the timeline (which will be the case if
// opts.pendingEventOrdering==chronological), we need to update the
// timeline map.
for (var i = 0; i < this._timelineSets.length; i++) {
for (let i = 0; i < this._timelineSets.length; i++) {
this._timelineSets[i].replaceEventId(oldEventId, newEventId);
}
}
else if (newStatus == EventStatus.CANCELLED) {
} else if (newStatus == EventStatus.CANCELLED) {
// remove it from the pending event list, or the timeline.
if (this._pendingEventList) {
utils.removeElement(
this._pendingEventList,
function(ev) { return ev.getId() == oldEventId; },
false
function(ev) {
return ev.getId() == oldEventId;
}, false,
);
}
this.removeEvent(oldEventId);
@@ -807,24 +827,24 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
*/
Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
var i;
let i;
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
}
// sanity check that the live timeline is still live
for (i = 0; i < this._timelineSets.length; i++) {
var liveTimeline = this._timelineSets[i].getLiveTimeline();
const liveTimeline = this._timelineSets[i].getLiveTimeline();
if (liveTimeline.getPaginationToken(EventTimeline.FORWARDS)) {
throw new Error(
"live timeline " + i + " is no longer live - it has a pagination token " +
"(" + liveTimeline.getPaginationToken(EventTimeline.FORWARDS) + ")"
"(" + liveTimeline.getPaginationToken(EventTimeline.FORWARDS) + ")",
);
}
if (liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) {
throw new Error(
"live timeline " + i + " is no longer live - " +
"it has a neighbouring timeline"
"it has a neighbouring timeline",
);
}
}
@@ -832,8 +852,7 @@ Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
for (i = 0; i < events.length; i++) {
if (events[i].getType() === "m.typing") {
this.currentState.setTypingEvent(events[i]);
}
else if (events[i].getType() === "m.receipt") {
} else if (events[i].getType() === "m.receipt") {
this.addReceipt(events[i]);
}
// N.B. account_data is added directly by /sync to avoid
@@ -851,7 +870,7 @@ Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
* @param {String[]} event_ids A list of event_ids to remove.
*/
Room.prototype.removeEvents = function(event_ids) {
for (var i = 0; i < event_ids.length; ++i) {
for (let i = 0; i < event_ids.length; ++i) {
this.removeEvent(event_ids[i]);
}
};
@@ -864,9 +883,9 @@ Room.prototype.removeEvents = function(event_ids) {
* @return {bool} true if the event was removed from any of the room's timeline sets
*/
Room.prototype.removeEvent = function(eventId) {
var removedAny = false;
for (var i = 0; i < this._timelineSets.length; i++) {
var removed = this._timelineSets[i].removeEvent(eventId);
let removedAny = false;
for (let i = 0; i < this._timelineSets.length; i++) {
const removed = this._timelineSets[i].removeEvent(eventId);
if (removed) {
removedAny = true;
}
@@ -885,15 +904,15 @@ Room.prototype.removeEvent = function(eventId) {
Room.prototype.recalculate = function(userId) {
// set fake stripped state events if this is an invite room so logic remains
// consistent elsewhere.
var self = this;
var membershipEvent = this.currentState.getStateEvents(
"m.room.member", userId
const self = this;
const membershipEvent = this.currentState.getStateEvents(
"m.room.member", userId,
);
if (membershipEvent && membershipEvent.getContent().membership === "invite") {
var strippedStateEvents = membershipEvent.event.invite_room_state || [];
const strippedStateEvents = membershipEvent.event.invite_room_state || [];
utils.forEach(strippedStateEvents, function(strippedEvent) {
var existingEvent = self.currentState.getStateEvents(
strippedEvent.type, strippedEvent.state_key
const existingEvent = self.currentState.getStateEvents(
strippedEvent.type, strippedEvent.state_key,
);
if (!existingEvent) {
// set the fake stripped event instead
@@ -903,18 +922,16 @@ Room.prototype.recalculate = function(userId) {
content: strippedEvent.content,
event_id: "$fake" + Date.now(),
room_id: self.roomId,
user_id: userId // technically a lie
user_id: userId, // technically a lie
})]);
}
});
}
var oldName = this.name;
const oldName = this.name;
this.name = calculateRoomName(this, userId);
this.summary = new RoomSummary(this.roomId, {
title: this.name
title: this.name,
});
if (oldName !== this.name) {
@@ -946,7 +963,7 @@ Room.prototype.getUsersReadUpTo = function(event) {
* @return {String} ID of the latest event that the given user has read, or null.
*/
Room.prototype.getEventReadUpTo = function(userId, ignoreSynthesized) {
var receipts = this._receipts;
let receipts = this._receipts;
if (ignoreSynthesized) {
receipts = this._realReceipts;
}
@@ -987,7 +1004,9 @@ Room.prototype.addReceipt = function(event, fake) {
// }
// }
// }
if (fake === undefined) { fake = false; }
if (fake === undefined) {
fake = false;
}
if (!fake) {
this._addReceiptsToStructure(event, this._realReceipts);
// we don't bother caching real receipts by event ID
@@ -1007,18 +1026,18 @@ Room.prototype.addReceipt = function(event, fake) {
* @param {Object} receipts The object to add receipts to
*/
Room.prototype._addReceiptsToStructure = function(event, receipts) {
var self = this;
const self = this;
utils.keys(event.getContent()).forEach(function(eventId) {
utils.keys(event.getContent()[eventId]).forEach(function(receiptType) {
utils.keys(event.getContent()[eventId][receiptType]).forEach(
function(userId) {
var receipt = event.getContent()[eventId][receiptType][userId];
const receipt = event.getContent()[eventId][receiptType][userId];
if (!receipts[receiptType]) {
receipts[receiptType] = {};
}
var existingReceipt = receipts[receiptType][userId];
const existingReceipt = receipts[receiptType][userId];
if (!existingReceipt) {
receipts[receiptType][userId] = {};
@@ -1027,7 +1046,7 @@ Room.prototype._addReceiptsToStructure = function(event, receipts) {
// than the one we already have. (This is managed
// server-side, but because we synthesize RRs locally we
// have to do it here too.)
var ordering = self.getUnfilteredTimelineSet().compareEventOrdering(
const ordering = self.getUnfilteredTimelineSet().compareEventOrdering(
existingReceipt.eventId, eventId);
if (ordering !== null && ordering >= 0) {
return;
@@ -1036,7 +1055,7 @@ Room.prototype._addReceiptsToStructure = function(event, receipts) {
receipts[receiptType][userId] = {
eventId: eventId,
data: receipt
data: receipt,
};
});
});
@@ -1049,17 +1068,17 @@ Room.prototype._addReceiptsToStructure = function(event, receipts) {
* @return {Object} Map of receipts by event ID
*/
Room.prototype._buildReceiptCache = function(receipts) {
var receiptCacheByEventId = {};
const receiptCacheByEventId = {};
utils.keys(receipts).forEach(function(receiptType) {
utils.keys(receipts[receiptType]).forEach(function(userId) {
var receipt = receipts[receiptType][userId];
const receipt = receipts[receiptType][userId];
if (!receiptCacheByEventId[receipt.eventId]) {
receiptCacheByEventId[receipt.eventId] = [];
}
receiptCacheByEventId[receipt.eventId].push({
userId: userId,
type: receiptType,
data: receipt.data
data: receipt.data,
});
});
});
@@ -1104,8 +1123,8 @@ Room.prototype.addTags = function(event) {
* @param {Array<MatrixEvent>} events an array of account_data events to add
*/
Room.prototype.addAccountData = function(events) {
for (var i = 0; i < events.length; i++) {
var event = events[i];
for (let i = 0; i < events.length; i++) {
const event = events[i];
if (event.getType() === "m.tag") {
this.addTags(event);
}
@@ -1137,16 +1156,16 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
if (!ignoreRoomNameEvent) {
// check for an alias, if any. for now, assume first alias is the
// official one.
var mRoomName = room.currentState.getStateEvents("m.room.name", "");
const mRoomName = room.currentState.getStateEvents("m.room.name", "");
if (mRoomName && mRoomName.getContent() && mRoomName.getContent().name) {
return mRoomName.getContent().name;
}
}
var alias = room.getCanonicalAlias();
let alias = room.getCanonicalAlias();
if (!alias) {
var aliases = room.getAliases();
const aliases = room.getAliases();
if (aliases.length) {
alias = aliases[0];
@@ -1157,16 +1176,18 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
}
// get members that are NOT ourselves and are actually in the room.
var otherMembers = utils.filter(room.currentState.getMembers(), function(m) {
return (m.userId !== userId && m.membership !== "leave");
const otherMembers = utils.filter(room.currentState.getMembers(), function(m) {
return (
m.userId !== userId && m.membership !== "leave" && m.membership !== "ban"
);
});
var allMembers = utils.filter(room.currentState.getMembers(), function(m) {
const allMembers = utils.filter(room.currentState.getMembers(), function(m) {
return (m.membership !== "leave");
});
var myMemberEventArray = utils.filter(room.currentState.getMembers(), function(m) {
const myMemberEventArray = utils.filter(room.currentState.getMembers(), function(m) {
return (m.userId == userId);
});
var myMemberEvent = (
const myMemberEvent = (
(myMemberEventArray.length && myMemberEventArray[0].events) ?
myMemberEventArray[0].events.member.event : undefined
);
@@ -1176,7 +1197,7 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
if (room.currentState.getMember(myMemberEvent.sender)) {
// extract who invited us to the room
return "Invite from " + room.currentState.getMember(
myMemberEvent.sender
myMemberEvent.sender,
).name;
} else if (allMembers[0].events.member) {
// use the sender field from the invite event, although this only
@@ -1193,45 +1214,38 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
// self-chat, peeked room with 1 participant,
// or inbound invite, or outbound 3PID invite.
if (allMembers[0].userId === userId) {
var thirdPartyInvites =
const thirdPartyInvites =
room.currentState.getStateEvents("m.room.third_party_invite");
if (thirdPartyInvites && thirdPartyInvites.length > 0) {
var name = "Inviting " +
let name = "Inviting " +
thirdPartyInvites[0].getContent().display_name;
if (thirdPartyInvites.length > 1) {
if (thirdPartyInvites.length == 2) {
name += " and " +
thirdPartyInvites[1].getContent().display_name;
}
else {
} else {
name += " and " +
thirdPartyInvites.length + " others";
}
}
return name;
}
else {
} else {
return "Empty room";
}
}
else {
} else {
return allMembers[0].name;
}
}
else {
} else {
// there really isn't anyone in this room...
return "Empty room";
}
}
else if (otherMembers.length === 1) {
} else if (otherMembers.length === 1) {
return otherMembers[0].name;
}
else if (otherMembers.length === 2) {
} else if (otherMembers.length === 2) {
return (
otherMembers[0].name + " and " + otherMembers[1].name
);
}
else {
} else {
return (
otherMembers[0].name + " and " + (otherMembers.length - 1) + " others"
);
@@ -1248,11 +1262,11 @@ function reEmit(reEmitEntity, emittableEntity, eventNames) {
// Transformation Example:
// listener on "foo" => function(a,b) { ... }
// Re-emit on "thing" => thing.emit("foo", a, b)
var newArgs = [eventName];
for (var i = 0; i < arguments.length; i++) {
const newArgs = [eventName];
for (let i = 0; i < arguments.length; i++) {
newArgs.push(arguments[i]);
}
reEmitEntity.emit.apply(reEmitEntity, newArgs);
reEmitEntity.emit(...newArgs);
});
});
}
@@ -19,8 +19,8 @@ limitations under the License.
* @module models/search-result
*/
var EventContext = require("./event-context");
var utils = require("../utils");
const EventContext = require("./event-context");
const utils = require("../utils");
/**
* Construct a new SearchResult
@@ -45,11 +45,11 @@ function SearchResult(rank, eventContext) {
*/
SearchResult.fromJson = function(jsonObj, eventMapper) {
var jsonContext = jsonObj.context || {};
var events_before = jsonContext.events_before || [];
var events_after = jsonContext.events_after || [];
const jsonContext = jsonObj.context || {};
const events_before = jsonContext.events_before || [];
const events_after = jsonContext.events_after || [];
var context = new EventContext(eventMapper(jsonObj.result));
const context = new EventContext(eventMapper(jsonObj.result));
context.setPaginateToken(jsonContext.start, true);
context.addEvents(utils.map(events_before, eventMapper), true);
+11 -14
View File
@@ -17,8 +17,8 @@ limitations under the License.
/**
* @module models/user
*/
var EventEmitter = require("events").EventEmitter;
var utils = require("../utils");
const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
/**
* Construct a new User. A User must have an ID and can optionally have extra
@@ -54,7 +54,7 @@ function User(userId) {
this.currentlyActive = false;
this.events = {
presence: null,
profile: null
profile: null,
};
this._updateModifiedTime();
}
@@ -73,26 +73,23 @@ User.prototype.setPresenceEvent = function(event) {
if (event.getType() !== "m.presence") {
return;
}
var firstFire = this.events.presence === null;
const firstFire = this.events.presence === null;
this.events.presence = event;
var eventsToFire = [];
const eventsToFire = [];
if (event.getContent().presence !== this.presence || firstFire) {
eventsToFire.push("User.presence");
}
if (event.getContent().avatar_url &&
event.getContent().avatar_url !== this.avatarUrl)
{
event.getContent().avatar_url !== this.avatarUrl) {
eventsToFire.push("User.avatarUrl");
}
if (event.getContent().displayname &&
event.getContent().displayname !== this.displayName)
{
event.getContent().displayname !== this.displayName) {
eventsToFire.push("User.displayName");
}
if (event.getContent().currently_active !== undefined &&
event.getContent().currently_active !== this.currentlyActive)
{
event.getContent().currently_active !== this.currentlyActive) {
eventsToFire.push("User.currentlyActive");
}
@@ -114,7 +111,7 @@ User.prototype.setPresenceEvent = function(event) {
this._updateModifiedTime();
for (var i = 0; i < eventsToFire.length; i++) {
for (let i = 0; i < eventsToFire.length; i++) {
this.emit(eventsToFire[i], event, this);
}
};
@@ -125,7 +122,7 @@ User.prototype.setPresenceEvent = function(event) {
* @param {string} name The new display name.
*/
User.prototype.setDisplayName = function(name) {
var oldName = this.displayName;
const oldName = this.displayName;
this.displayName = name;
if (name !== oldName) {
this._updateModifiedTime();
@@ -149,7 +146,7 @@ User.prototype.setRawDisplayName = function(name) {
* @param {string} url The new avatar URL.
*/
User.prototype.setAvatarUrl = function(url) {
var oldUrl = this.avatarUrl;
const oldUrl = this.avatarUrl;
this.avatarUrl = url;
if (url !== oldUrl) {
this._updateModifiedTime();
+111 -77
View File
@@ -23,24 +23,28 @@ limitations under the License.
* @param {Object} client The Matrix client object to use
*/
function PushProcessor(client) {
var escapeRegExp = function(string) {
const escapeRegExp = function(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
var matchingRuleFromKindSet = function(ev, kindset, device) {
var rulekinds_in_order = ['override', 'content', 'room', 'sender', 'underride'];
for (var ruleKindIndex = 0;
const matchingRuleFromKindSet = function(ev, kindset, device) {
const rulekinds_in_order = ['override', 'content', 'room', 'sender', 'underride'];
for (let ruleKindIndex = 0;
ruleKindIndex < rulekinds_in_order.length;
++ruleKindIndex) {
var kind = rulekinds_in_order[ruleKindIndex];
var ruleset = kindset[kind];
const kind = rulekinds_in_order[ruleKindIndex];
const ruleset = kindset[kind];
for (var ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) {
var rule = ruleset[ruleIndex];
if (!rule.enabled) { continue; }
for (let ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) {
const rule = ruleset[ruleIndex];
if (!rule.enabled) {
continue;
}
var rawrule = templateRuleToRaw(kind, rule, device);
if (!rawrule) { continue; }
const rawrule = templateRuleToRaw(kind, rule, device);
if (!rawrule) {
continue;
}
if (ruleMatchesEvent(rawrule, ev)) {
rule.kind = kind;
@@ -51,11 +55,11 @@ function PushProcessor(client) {
return null;
};
var templateRuleToRaw = function(kind, tprule, device) {
var rawrule = {
const templateRuleToRaw = function(kind, tprule, device) {
const rawrule = {
'rule_id': tprule.rule_id,
'actions': tprule.actions,
'conditions': []
'conditions': [],
};
switch (kind) {
case 'underride':
@@ -63,55 +67,61 @@ function PushProcessor(client) {
rawrule.conditions = tprule.conditions;
break;
case 'room':
if (!tprule.rule_id) { return null; }
if (!tprule.rule_id) {
return null;
}
rawrule.conditions.push({
'kind': 'event_match',
'key': 'room_id',
'pattern': tprule.rule_id
'pattern': tprule.rule_id,
});
break;
case 'sender':
if (!tprule.rule_id) { return null; }
if (!tprule.rule_id) {
return null;
}
rawrule.conditions.push({
'kind': 'event_match',
'key': 'user_id',
'pattern': tprule.rule_id
'pattern': tprule.rule_id,
});
break;
case 'content':
if (!tprule.pattern) { return null; }
if (!tprule.pattern) {
return null;
}
rawrule.conditions.push({
'kind': 'event_match',
'key': 'content.body',
'pattern': tprule.pattern
'pattern': tprule.pattern,
});
break;
}
if (device) {
rawrule.conditions.push({
'kind': 'device',
'profile_tag': device
'profile_tag': device,
});
}
return rawrule;
};
var ruleMatchesEvent = function(rule, ev) {
var ret = true;
for (var i = 0; i < rule.conditions.length; ++i) {
var cond = rule.conditions[i];
const ruleMatchesEvent = function(rule, ev) {
let ret = true;
for (let i = 0; i < rule.conditions.length; ++i) {
const cond = rule.conditions[i];
ret &= eventFulfillsCondition(cond, ev);
}
//console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match"));
return ret;
};
var eventFulfillsCondition = function(cond, ev) {
var condition_functions = {
const eventFulfillsCondition = function(cond, ev) {
const condition_functions = {
"event_match": eventFulfillsEventMatchCondition,
"device": eventFulfillsDeviceCondition,
"contains_display_name": eventFulfillsDisplayNameCondition,
"room_member_count": eventFulfillsRoomMemberCountCondition
"room_member_count": eventFulfillsRoomMemberCountCondition,
};
if (condition_functions[cond.kind]) {
return condition_functions[cond.kind](cond, ev);
@@ -119,21 +129,29 @@ function PushProcessor(client) {
return true;
};
var eventFulfillsRoomMemberCountCondition = function(cond, ev) {
if (!cond.is) { return false; }
const eventFulfillsRoomMemberCountCondition = function(cond, ev) {
if (!cond.is) {
return false;
}
var room = client.getRoom(ev.getRoomId());
if (!room || !room.currentState || !room.currentState.members) { return false; }
const room = client.getRoom(ev.getRoomId());
if (!room || !room.currentState || !room.currentState.members) {
return false;
}
var memberCount = Object.keys(room.currentState.members).filter(function(m) {
const memberCount = Object.keys(room.currentState.members).filter(function(m) {
return room.currentState.members[m].membership == 'join';
}).length;
var m = cond.is.match(/^([=<>]*)([0-9]*)$/);
if (!m) { return false; }
var ineq = m[1];
var rhs = parseInt(m[2]);
if (isNaN(rhs)) { return false; }
const m = cond.is.match(/^([=<>]*)([0-9]*)$/);
if (!m) {
return false;
}
const ineq = m[1];
const rhs = parseInt(m[2]);
if (isNaN(rhs)) {
return false;
}
switch (ineq) {
case '':
case '==':
@@ -151,64 +169,68 @@ function PushProcessor(client) {
}
};
var eventFulfillsDisplayNameCondition = function(cond, ev) {
var content = ev.getContent();
const eventFulfillsDisplayNameCondition = function(cond, ev) {
const content = ev.getContent();
if (!content || !content.body || typeof content.body != 'string') {
return false;
}
var room = client.getRoom(ev.getRoomId());
const room = client.getRoom(ev.getRoomId());
if (!room || !room.currentState || !room.currentState.members ||
!room.currentState.getMember(client.credentials.userId)) { return false; }
!room.currentState.getMember(client.credentials.userId)) {
return false;
}
var displayName = room.currentState.getMember(client.credentials.userId).name;
const displayName = room.currentState.getMember(client.credentials.userId).name;
// 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');
const pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i');
return content.body.search(pat) > -1;
};
var eventFulfillsDeviceCondition = function(cond, ev) {
const eventFulfillsDeviceCondition = function(cond, ev) {
return false; // XXX: Allow a profile tag to be set for the web client instance
};
var eventFulfillsEventMatchCondition = function(cond, ev) {
var val = valueForDottedKey(cond.key, ev);
if (!val || typeof val != 'string') { return false; }
const eventFulfillsEventMatchCondition = function(cond, ev) {
const val = valueForDottedKey(cond.key, ev);
if (!val || typeof val != 'string') {
return false;
}
var pat;
let pat;
if (cond.key == 'content.body') {
pat = '(^|\\W)' + globToRegexp(cond.pattern) + '(\\W|$)';
} else {
pat = '^' + globToRegexp(cond.pattern) + '$';
}
var regex = new RegExp(pat, 'i');
const regex = new RegExp(pat, 'i');
return !!val.match(regex);
};
var globToRegexp = function(glob) {
const globToRegexp = function(glob) {
// From
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
// Because micromatch is about 130KB with dependencies,
// and minimatch is not much better.
var pat = escapeRegExp(glob);
let pat = escapeRegExp(glob);
pat = pat.replace(/\\\*/, '.*');
pat = pat.replace(/\?/, '.');
pat = pat.replace(/\\\[(!|)(.*)\\]/, function(match, p1, p2, offset, string) {
var first = p1 && '^' || '';
var second = p2.replace(/\\\-/, '-');
const first = p1 && '^' || '';
const second = p2.replace(/\\\-/, '-');
return '[' + first + second + ']';
});
return pat;
};
var valueForDottedKey = function(key, ev) {
var parts = key.split('.');
var val;
const valueForDottedKey = function(key, ev) {
const parts = key.split('.');
let val;
// special-case the first component to deal with encrypted messages
var firstPart = parts[0];
const firstPart = parts[0];
if (firstPart == 'content') {
val = ev.getContent();
parts.shift();
@@ -221,33 +243,43 @@ function PushProcessor(client) {
}
while (parts.length > 0) {
var thispart = parts.shift();
if (!val[thispart]) { return null; }
const thispart = parts.shift();
if (!val[thispart]) {
return null;
}
val = val[thispart];
}
return val;
};
var matchingRuleForEventWithRulesets = function(ev, rulesets) {
if (!rulesets || !rulesets.device) { return null; }
if (ev.getSender() == client.credentials.userId) { return null; }
const matchingRuleForEventWithRulesets = function(ev, rulesets) {
if (!rulesets || !rulesets.device) {
return null;
}
if (ev.getSender() == client.credentials.userId) {
return null;
}
var allDevNames = Object.keys(rulesets.device);
for (var i = 0; i < allDevNames.length; ++i) {
var devname = allDevNames[i];
var devrules = rulesets.device[devname];
const allDevNames = Object.keys(rulesets.device);
for (let i = 0; i < allDevNames.length; ++i) {
const devname = allDevNames[i];
const devrules = rulesets.device[devname];
var matchingRule = matchingRuleFromKindSet(devrules, devname);
if (matchingRule) { return matchingRule; }
const matchingRule = matchingRuleFromKindSet(devrules, devname);
if (matchingRule) {
return matchingRule;
}
}
return matchingRuleFromKindSet(ev, rulesets.global);
};
var pushActionsForEventAndRulesets = function(ev, rulesets) {
var rule = matchingRuleForEventWithRulesets(ev, rulesets);
if (!rule) { return {}; }
const pushActionsForEventAndRulesets = function(ev, rulesets) {
const rule = matchingRuleForEventWithRulesets(ev, rulesets);
if (!rule) {
return {};
}
var actionObj = PushProcessor.actionListToActionsObject(rule.actions);
const actionObj = PushProcessor.actionListToActionsObject(rule.actions);
// Some actions are implicit in some situations: we add those here
if (actionObj.tweaks.highlight === undefined) {
@@ -280,13 +312,15 @@ function PushProcessor(client) {
* @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];
const actionobj = { 'notify': false, 'tweaks': {} };
for (let i = 0; i < actionlist.length; ++i) {
const action = actionlist[i];
if (action === 'notify') {
actionobj.notify = true;
} else if (typeof action === 'object') {
if (action.value === undefined) { action.value = true; }
if (action.value === undefined) {
action.value = true;
}
actionobj.tweaks[action.set_tweak] = action.value;
}
}
@@ -27,20 +27,20 @@ limitations under the License.
// we schedule a callback at least this often, to check if we've missed out on
// some wall-clock time due to being suspended.
var TIMER_CHECK_PERIOD_MS = 1000;
const TIMER_CHECK_PERIOD_MS = 1000;
// counter, for making up ids to return from setTimeout
var _count = 0;
let _count = 0;
// the key for our callback with the real global.setTimeout
var _realCallbackKey;
let _realCallbackKey;
// a sorted list of the callbacks to be run.
// each is an object with keys [runAt, func, params, key].
var _callbackList = [];
const _callbackList = [];
// var debuglog = console.log.bind(console);
var debuglog = function() {};
const debuglog = function() {};
/**
* Replace the function used by this module to get the current time.
@@ -54,7 +54,7 @@ var debuglog = function() {};
module.exports.setNow = function(f) {
_now = f || Date.now;
};
var _now = Date.now;
let _now = Date.now;
/**
* reimplementation of window.setTimeout, which will call the callback if
@@ -72,12 +72,12 @@ module.exports.setTimeout = function(func, delayMs) {
delayMs = 0;
}
var params = Array.prototype.slice.call(arguments, 2);
var runAt = _now() + delayMs;
var key = _count++;
const params = Array.prototype.slice.call(arguments, 2);
const runAt = _now() + delayMs;
const key = _count++;
debuglog("setTimeout: scheduling cb", key, "at", runAt,
"(delay", delayMs, ")");
var data = {
const data = {
runAt: runAt,
func: func,
params: params,
@@ -85,10 +85,10 @@ module.exports.setTimeout = function(func, delayMs) {
};
// figure out where it goes in the list
var idx = binarySearch(
const idx = binarySearch(
_callbackList, function(el) {
return el.runAt - runAt;
}
},
);
_callbackList.splice(idx, 0, data);
@@ -108,9 +108,9 @@ module.exports.clearTimeout = function(key) {
}
// remove the element from the list
var i;
let i;
for (i = 0; i < _callbackList.length; i++) {
var cb = _callbackList[i];
const cb = _callbackList[i];
if (cb.key == key) {
_callbackList.splice(i, 1);
break;
@@ -129,29 +129,29 @@ function _scheduleRealCallback() {
global.clearTimeout(_realCallbackKey);
}
var first = _callbackList[0];
const first = _callbackList[0];
if (!first) {
debuglog("_scheduleRealCallback: no more callbacks, not rescheduling");
return;
}
var now = _now();
var delayMs = Math.min(first.runAt - now, TIMER_CHECK_PERIOD_MS);
const now = _now();
const delayMs = Math.min(first.runAt - now, TIMER_CHECK_PERIOD_MS);
debuglog("_scheduleRealCallback: now:", now, "delay:", delayMs);
_realCallbackKey = global.setTimeout(_runCallbacks, delayMs);
}
function _runCallbacks() {
var cb;
var now = _now();
let cb;
const now = _now();
debuglog("_runCallbacks: now:", now);
// get the list of things to call
var callbacksToRun = [];
const callbacksToRun = [];
while (true) {
var first = _callbackList[0];
const first = _callbackList[0];
if (!first || first.runAt > now) {
break;
}
@@ -165,10 +165,10 @@ function _runCallbacks() {
// register their own setTimeouts.
_scheduleRealCallback();
for (var i = 0; i < callbacksToRun.length; i++) {
for (let i = 0; i < callbacksToRun.length; i++) {
cb = callbacksToRun[i];
try {
cb.func.apply(null, cb.params);
cb.func.apply(global, cb.params);
} catch (e) {
console.error("Uncaught exception in callback function",
e.stack || e);
@@ -184,12 +184,12 @@ function _runCallbacks() {
*/
function binarySearch(array, func) {
// min is inclusive, max exclusive.
var min = 0,
let min = 0,
max = array.length;
while (min < max) {
var mid = (min + max) >> 1;
var res = func(array[mid]);
const mid = (min + max) >> 1;
const res = func(array[mid]);
if (res > 0) {
// the element at 'mid' is too big; set it as the new max.
max = mid;
+21 -22
View File
@@ -19,10 +19,10 @@ limitations under the License.
* of requests.
* @module scheduler
*/
var utils = require("./utils");
var q = require("q");
const utils = require("./utils");
const q = require("q");
var DEBUG = false; // set true to enable console logging.
const DEBUG = false; // set true to enable console logging.
/**
* Construct a scheduler for Matrix. Requires
@@ -60,7 +60,7 @@ function MatrixScheduler(retryAlgorithm, queueAlgorithm) {
* @see MatrixScheduler.removeEventFromQueue To remove an event from the queue.
*/
MatrixScheduler.prototype.getQueueForEvent = function(event) {
var name = this.queueAlgorithm(event);
const name = this.queueAlgorithm(event);
if (!name || !this._queues[name]) {
return null;
}
@@ -76,11 +76,11 @@ MatrixScheduler.prototype.getQueueForEvent = function(event) {
* @return {boolean} True if this event was removed.
*/
MatrixScheduler.prototype.removeEventFromQueue = function(event) {
var name = this.queueAlgorithm(event);
const name = this.queueAlgorithm(event);
if (!name || !this._queues[name]) {
return false;
}
var removed = false;
let removed = false;
utils.removeElement(this._queues[name], function(element) {
if (element.event.getId() === event.getId()) {
removed = true;
@@ -110,7 +110,7 @@ MatrixScheduler.prototype.setProcessFunction = function(fn) {
* resolved or rejected in due time, else null.
*/
MatrixScheduler.prototype.queueEvent = function(event) {
var queueName = this.queueAlgorithm(event);
const queueName = this.queueAlgorithm(event);
if (!queueName) {
return null;
}
@@ -118,15 +118,15 @@ MatrixScheduler.prototype.queueEvent = function(event) {
if (!this._queues[queueName]) {
this._queues[queueName] = [];
}
var defer = q.defer();
const defer = q.defer();
this._queues[queueName].push({
event: event,
defer: defer,
attempts: 0
attempts: 0,
});
debuglog(
"Queue algorithm dumped event %s into queue '%s'",
event.getId(), queueName
event.getId(), queueName,
);
_startProcessingQueues(this);
return defer.promise;
@@ -155,7 +155,7 @@ MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
}
if (err.name === "M_LIMIT_EXCEEDED") {
var waitTime = err.data.retry_after_ms;
const waitTime = err.data.retry_after_ms;
if (waitTime) {
return waitTime;
}
@@ -201,10 +201,10 @@ function _startProcessingQueues(scheduler) {
function _processQueue(scheduler, queueName) {
// get head of queue
var obj = _peekNextEvent(scheduler, queueName);
const obj = _peekNextEvent(scheduler, queueName);
if (!obj) {
// queue is empty. Mark as inactive and stop recursing.
var index = scheduler._activeQueues.indexOf(queueName);
const index = scheduler._activeQueues.indexOf(queueName);
if (index >= 0) {
scheduler._activeQueues.splice(index, 1);
}
@@ -213,7 +213,7 @@ function _processQueue(scheduler, queueName) {
}
debuglog(
"Queue '%s' has %s pending events",
queueName, scheduler._queues[queueName].length
queueName, scheduler._queues[queueName].length,
);
// fire the process function and if it resolves, resolve the deferred. Else
// invoke the retry algorithm.
@@ -227,22 +227,21 @@ function _processQueue(scheduler, queueName) {
}, function(err) {
obj.attempts += 1;
// ask the retry algorithm when/if we should try again
var waitTimeMs = scheduler.retryAlgorithm(obj.event, obj.attempts, err);
const waitTimeMs = scheduler.retryAlgorithm(obj.event, obj.attempts, err);
debuglog(
"retry(%s) err=%s event_id=%s waitTime=%s",
obj.attempts, err, obj.event.getId(), waitTimeMs
obj.attempts, err, obj.event.getId(), waitTimeMs,
);
if (waitTimeMs === -1) { // give up (you quitter!)
debuglog(
"Queue '%s' giving up on event %s", queueName, obj.event.getId()
"Queue '%s' giving up on event %s", queueName, obj.event.getId(),
);
// remove this from the queue
_removeNextEvent(scheduler, queueName);
obj.defer.reject(err);
// process next event
_processQueue(scheduler, queueName);
}
else {
} else {
setTimeout(function() {
_processQueue(scheduler, queueName);
}, waitTimeMs);
@@ -251,7 +250,7 @@ function _processQueue(scheduler, queueName) {
}
function _peekNextEvent(scheduler, queueName) {
var queue = scheduler._queues[queueName];
const queue = scheduler._queues[queueName];
if (!utils.isArray(queue)) {
return null;
}
@@ -259,7 +258,7 @@ function _peekNextEvent(scheduler, queueName) {
}
function _removeNextEvent(scheduler, queueName) {
var queue = scheduler._queues[queueName];
const queue = scheduler._queues[queueName];
if (!utils.isArray(queue)) {
return null;
}
@@ -268,7 +267,7 @@ function _removeNextEvent(scheduler, queueName) {
function debuglog() {
if (DEBUG) {
console.log.apply(console, arguments);
console.log(...arguments);
}
}
+318
View File
@@ -0,0 +1,318 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import q from "q";
import SyncAccumulator from "../sync-accumulator";
import utils from "../utils";
const VERSION = 1;
function createDatabase(db) {
// Make user store, clobber based on user ID. (userId property of User objects)
db.createObjectStore("users", { keyPath: ["userId"] });
// Make account data store, clobber based on event type.
// (event.type property of MatrixEvent objects)
db.createObjectStore("accountData", { keyPath: ["type"] });
// Make /sync store (sync tokens, room data, etc), always clobber (const key).
db.createObjectStore("sync", { keyPath: ["clobber"] });
}
/**
* Helper method to collect results from a Cursor and promiseify it.
* @param {ObjectStore|Index} store The store to perform openCursor on.
* @param {IDBKeyRange=} keyRange Optional key range to apply on the cursor.
* @param {Function} resultMapper A function which is repeatedly called with a
* Cursor.
* Return the data you want to keep.
* @return {Promise<T[]>} Resolves to an array of whatever you returned from
* resultMapper.
*/
function selectQuery(store, keyRange, resultMapper) {
const query = store.openCursor(keyRange);
return q.Promise((resolve, reject) => { /*eslint new-cap: 0*/
const results = [];
query.onerror = (event) => {
reject(new Error("Query failed: " + event.target.errorCode));
};
// collect results
query.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
resolve(results);
return; // end of results
}
results.push(resultMapper(cursor));
cursor.continue();
};
});
}
function promiseifyTxn(txn) {
return new q.Promise((resolve, reject) => {
txn.oncomplete = function(event) {
resolve(event);
};
txn.onerror = function(event) {
reject(event);
};
});
}
function promiseifyRequest(req) {
return new q.Promise((resolve, reject) => {
req.onsuccess = function(event) {
resolve(event);
};
req.onerror = function(event) {
reject(event);
};
});
}
/**
* Does the actual reading from and writing to the indexeddb
*
* Construct a new Indexed Database store backend. This requires a call to
* <code>connect()</code> before this store can be used.
* @constructor
* @param {Object} indexedDBInterface The Indexed DB interface e.g
* <code>window.indexedDB</code>
* @param {string=} dbName Optional database name. The same name must be used
* to open the same database.
*/
const LocalIndexedDBStoreBackend = function LocalIndexedDBStoreBackend(
indexedDBInterface, dbName,
) {
this.indexedDB = indexedDBInterface;
this._dbName = "matrix-js-sdk:" + (dbName || "default");
this.db = null;
this._syncAccumulator = new SyncAccumulator();
};
LocalIndexedDBStoreBackend.prototype = {
/**
* Attempt to connect to the database. This can fail if the user does not
* grant permission.
* @return {Promise} Resolves if successfully connected.
*/
connect: function() {
if (this.db) {
return q();
}
const req = this.indexedDB.open(this._dbName, VERSION);
req.onupgradeneeded = (ev) => {
const db = ev.target.result;
const oldVersion = ev.oldVersion;
if (oldVersion < 1) { // The database did not previously exist.
createDatabase(db);
}
// Expand as needed.
};
return promiseifyRequest(req).then((ev) => {
this.db = ev.target.result;
// add a poorly-named listener for when deleteDatabase is called
// so we can close our db connections.
this.db.onversionchange = () => {
this.db.close();
};
return this._init();
});
},
/**
* Having connected, load initial data from the database and prepare for use
* @return {Promise} Resolves on success
*/
_init: function() {
return q.all([
this._loadAccountData(),
this._loadSyncData(),
]).then(([accountData, syncData]) => {
this._syncAccumulator.accumulate({
next_batch: syncData.nextBatch,
rooms: syncData.roomsData,
account_data: {
events: accountData,
},
});
});
},
/**
* Clear the entire database. This should be used when logging out of a client
* to prevent mixing data between accounts.
* @return {Promise} Resolved when the database is cleared.
*/
clearDatabase: function() {
console.log("Removing indexeddb instance: ", this._dbName);
return promiseifyRequest(this.indexedDB.deleteDatabase(this._dbName));
},
/**
* @param {boolean=} copy If false, the data returned is from internal
* buffers and must not be muated. Otherwise, a copy is made before
* returning such that the data can be safely mutated. Default: true.
*
* @return {Promise} Resolves with a sync response to restore the
* client state to where it was at the last save, or null if there
* is no saved sync data.
*/
getSavedSync: function(copy) {
if (copy === undefined) copy = true;
const data = this._syncAccumulator.getJSON();
if (!data.nextBatch) return q(null);
if (copy) {
// We must deep copy the stored data so that the /sync processing code doesn't
// corrupt the internal state of the sync accumulator (it adds non-clonable keys)
return q(utils.deepCopy(data));
} else {
return q(data);
}
},
setSyncData: function(syncData) {
return q().then(() => {
this._syncAccumulator.accumulate(syncData);
});
},
syncToDatabase: function(userTuples) {
const syncData = this._syncAccumulator.getJSON();
return q.all([
this._persistUserPresenceEvents(userTuples),
this._persistAccountData(syncData.accountData),
this._persistSyncData(syncData.nextBatch, syncData.roomsData),
]);
},
/**
* Persist rooms /sync data along with the next batch token.
* @param {string} nextBatch The next_batch /sync value.
* @param {Object} roomsData The 'rooms' /sync data from a SyncAccumulator
* @return {Promise} Resolves if the data was persisted.
*/
_persistSyncData: function(nextBatch, roomsData) {
console.log("Persisting sync data up to ", nextBatch);
return q.try(() => {
const txn = this.db.transaction(["sync"], "readwrite");
const store = txn.objectStore("sync");
store.put({
clobber: "-", // constant key so will always clobber
nextBatch: nextBatch,
roomsData: roomsData,
}); // put == UPSERT
return promiseifyTxn(txn);
});
},
/**
* Persist a list of account data events. Events with the same 'type' will
* be replaced.
* @param {Object[]} accountData An array of raw user-scoped account data events
* @return {Promise} Resolves if the events were persisted.
*/
_persistAccountData: function(accountData) {
return q.try(() => {
const txn = this.db.transaction(["accountData"], "readwrite");
const store = txn.objectStore("accountData");
for (let i = 0; i < accountData.length; i++) {
store.put(accountData[i]); // put == UPSERT
}
return promiseifyTxn(txn);
});
},
/**
* Persist a list of [user id, presence event] they are for.
* Users with the same 'userId' will be replaced.
* Presence events should be the event in its raw form (not the Event
* object)
* @param {Object[]} tuples An array of [userid, event] tuples
* @return {Promise} Resolves if the users were persisted.
*/
_persistUserPresenceEvents: function(tuples) {
return q.try(() => {
const txn = this.db.transaction(["users"], "readwrite");
const store = txn.objectStore("users");
for (const tuple of tuples) {
store.put({
userId: tuple[0],
event: tuple[1],
}); // put == UPSERT
}
return promiseifyTxn(txn);
});
},
/**
* Load all user presence events from the database. This is not cached.
* FIXME: It would probably be more sensible to store the events in the
* sync.
* @return {Promise<Object[]>} A list of presence events in their raw form.
*/
getUserPresenceEvents: function() {
return q.try(() => {
const txn = this.db.transaction(["users"], "readonly");
const store = txn.objectStore("users");
return selectQuery(store, undefined, (cursor) => {
return [cursor.value.userId, cursor.value.event];
});
});
},
/**
* Load all the account data events from the database. This is not cached.
* @return {Promise<Object[]>} A list of raw global account events.
*/
_loadAccountData: function() {
return q.try(() => {
const txn = this.db.transaction(["accountData"], "readonly");
const store = txn.objectStore("accountData");
return selectQuery(store, undefined, (cursor) => {
return cursor.value;
});
});
},
/**
* Load the sync data from the database.
* @return {Promise<Object>} An object with "roomsData" and "nextBatch" keys.
*/
_loadSyncData: function() {
return q.try(() => {
const txn = this.db.transaction(["sync"], "readonly");
const store = txn.objectStore("sync");
return selectQuery(store, undefined, (cursor) => {
return cursor.value;
}).then((results) => {
if (results.length > 1) {
console.warn("loadSyncData: More than 1 sync row found.");
}
return (results.length > 0 ? results[0] : {});
});
});
},
};
export default LocalIndexedDBStoreBackend;
+140
View File
@@ -0,0 +1,140 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import q from "q";
/**
* An IndexedDB store backend where the actual backend sits in a web
* worker.
*
* Construct a new Indexed Database store backend. This requires a call to
* <code>connect()</code> before this store can be used.
* @constructor
* @param {string} workerScript URL to the worker script
* @param {string=} dbName Optional database name. The same name must be used
* to open the same database.
* @param {Object} WorkerApi The web worker compatible interface object
*/
const RemoteIndexedDBStoreBackend = function RemoteIndexedDBStoreBackend(
workerScript, dbName, WorkerApi,
) {
this._dbName = dbName;
this._worker = new WorkerApi(workerScript);
this._nextSeq = 0;
// The currently in-flight requests to the actual backend
this._inFlight = {
// seq: promise,
};
this._worker.onmessage = this._onWorkerMessage.bind(this);
};
RemoteIndexedDBStoreBackend.prototype = {
/**
* Attempt to connect to the database. This can fail if the user does not
* grant permission.
* @return {Promise} Resolves if successfully connected.
*/
connect: function() {
return this._doCmd('_setupWorker', [this._dbName]).then(() => {
console.log("IndexedDB worker is ready");
return this._doCmd('connect');
});
},
/**
* Clear the entire database. This should be used when logging out of a client
* to prevent mixing data between accounts.
* @return {Promise} Resolved when the database is cleared.
*/
clearDatabase: function() {
return this._doCmd('clearDatabase');
},
/**
* @return {Promise} Resolves with a sync response to restore the
* client state to where it was at the last save, or null if there
* is no saved sync data.
*/
getSavedSync: function() {
return this._doCmd('getSavedSync');
},
setSyncData: function(syncData) {
return this._doCmd('setSyncData', [syncData]);
},
syncToDatabase: function(users) {
return this._doCmd('syncToDatabase', [users]);
},
/**
* Load all user presence events from the database. This is not cached.
* @return {Promise<Object[]>} A list of presence events in their raw form.
*/
getUserPresenceEvents: function() {
return this._doCmd('getUserPresenceEvents');
},
_doCmd: function(cmd, args) {
// wrap in a q so if the postMessage throws,
// the promise automatically gets rejected
return q().then(() => {
const seq = this._nextSeq++;
const def = q.defer();
this._inFlight[seq] = def;
this._worker.postMessage({
command: cmd,
seq: seq,
args: args,
});
return def.promise;
});
},
_onWorkerMessage: function(ev) {
const msg = ev.data;
if (msg.command == 'cmd_success' || msg.command == 'cmd_fail') {
if (msg.seq === undefined) {
console.error("Got reply from worker with no seq");
return;
}
const def = this._inFlight[msg.seq];
if (def === undefined) {
console.error("Got reply for unknown seq " + msg.seq);
return;
}
delete this._inFlight[msg.seq];
if (msg.command == 'cmd_success') {
def.resolve(msg.result);
} else {
def.reject(msg.error);
}
} else {
console.warn("Unrecognised message from worker: " + msg);
}
},
};
export default RemoteIndexedDBStoreBackend;
+122
View File
@@ -0,0 +1,122 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import q from "q";
import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
/**
* This class lives in the webworker and drives a LocalIndexedDBStoreBackend
* controlled by messages from the main process.
*
* It should be instantiated by a web worker script provided by the application
* in a script, for example:
*
* import {IndexedDBStoreWorker} from 'matrix-js-sdk/lib/indexeddb-worker.js';
* const remoteWorker = new IndexedDBStoreWorker(postMessage);
* onmessage = remoteWorker.onMessage;
*
* Note that it is advisable to import this class by referencing the file directly to
* avoid a dependency on the whole js-sdk.
*
*/
class IndexedDBStoreWorker {
/**
* @param {function} postMessage The web worker postMessage function that
* should be used to communicate back to the main script.
*/
constructor(postMessage) {
this.backend = null;
this.postMessage = postMessage;
this.onMessage = this.onMessage.bind(this);
}
/**
* Passes a message event from the main script into the class. This method
* can be directly assigned to the web worker `onmessage` variable.
*
* @param {Object} ev The message event
*/
onMessage(ev) {
const msg = ev.data;
let prom;
switch (msg.command) {
case '_setupWorker':
this.backend = new LocalIndexedDBStoreBackend(
// this is the 'indexedDB' global (where global != window
// because it's a web worker and there is no window).
indexedDB, msg.args[0],
);
prom = q();
break;
case 'connect':
prom = this.backend.connect();
break;
case 'clearDatabase':
prom = this.backend.clearDatabase().then((result) => {
// This returns special classes which can't be cloned
// across to the main script, so don't try.
return {};
});
break;
case 'getSavedSync':
prom = this.backend.getSavedSync(false);
break;
case 'setSyncData':
prom = this.backend.setSyncData(...msg.args);
break;
case 'syncToDatabase':
prom = this.backend.syncToDatabase(...msg.args).then(() => {
// This also returns IndexedDB events which are not cloneable
return {};
});
break;
case 'getUserPresenceEvents':
prom = this.backend.getUserPresenceEvents();
break;
}
if (prom === undefined) {
postMessage({
command: 'cmd_fail',
seq: msg.seq,
// Can't be an Error because they're not structured cloneable
error: "Unrecognised command",
});
return;
}
prom.done((ret) => {
this.postMessage.call(null, {
command: 'cmd_success',
seq: msg.seq,
result: ret,
});
}, (err) => {
console.error("Error running command: "+msg.command);
console.error(err);
this.postMessage.call(null, {
command: 'cmd_fail',
seq: msg.seq,
// Just send a string because Error objects aren't cloneable
error: "Error running command",
});
});
}
}
module.exports = IndexedDBStoreWorker;
+195
View File
@@ -0,0 +1,195 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import q from "q";
import {MatrixInMemoryStore} from "./memory";
import utils from "../utils";
import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
import RemoteIndexedDBStoreBackend from "./indexeddb-remote-backend.js";
import User from "../models/user";
import {MatrixEvent} from "../models/event";
/**
* This is an internal module. See {@link IndexedDBStore} for the public class.
* @module store/indexeddb
*/
// If this value is too small we'll be writing very often which will cause
// noticable stop-the-world pauses. If this value is too big we'll be writing
// so infrequently that the /sync size gets bigger on reload. Writing more
// often does not affect the length of the pause since the entire /sync
// response is persisted each time.
const WRITE_DELAY_MS = 1000 * 60 * 5; // once every 5 minutes
/**
* Construct a new Indexed Database store, which extends MatrixInMemoryStore.
*
* This store functions like a MatrixInMemoryStore except it periodically persists
* the contents of the store to an IndexedDB backend.
*
* All data is still kept in-memory but can be loaded from disk by calling
* <code>startup()</code>. This can make startup times quicker as a complete
* sync from the server is not required. This does not reduce memory usage as all
* the data is eagerly fetched when <code>startup()</code> is called.
* <pre>
* let opts = { localStorage: window.localStorage };
* let store = new IndexedDBStore();
* await store.startup(); // load from indexed db
* let client = sdk.createClient({
* store: store,
* });
* client.startClient();
* client.on("sync", function(state, prevState, data) {
* if (state === "PREPARED") {
* console.log("Started up, now with go faster stripes!");
* }
* });
* </pre>
*
* @constructor
* @extends MatrixInMemoryStore
* @param {Object} opts Options object.
* @param {Object} opts.indexedDB The Indexed DB interface e.g.
* <code>window.indexedDB</code>
* @param {string=} opts.dbName Optional database name. The same name must be used
* to open the same database.
* @param {string=} opts.workerScript Optional URL to a script to invoke a web
* worker with to run IndexedDB queries on the web worker. The IndexedDbStoreWorker
* class is provided for this purpose and requires the application to provide a
* trivial wrapper script around it.
* @param {Object=} opts.workerApi The webWorker API object. If omitted, the global Worker
* object will be used if it exists.
* @prop {IndexedDBStoreBackend} backend The backend instance. Call through to
* this API if you need to perform specific indexeddb actions like deleting the
* database.
*/
const IndexedDBStore = function IndexedDBStore(opts) {
MatrixInMemoryStore.call(this, opts);
if (!opts.indexedDB) {
throw new Error('Missing required option: indexedDB');
}
if (opts.workerScript) {
// try & find a webworker-compatible API
let workerApi = opts.workerApi;
if (!workerApi) {
// default to the global Worker object (which is where it in a browser)
workerApi = global.Worker;
}
this.backend = new RemoteIndexedDBStoreBackend(
opts.workerScript, opts.dbName, workerApi,
);
} else {
this.backend = new LocalIndexedDBStoreBackend(opts.indexedDB, opts.dbName);
}
this.startedUp = false;
this._syncTs = 0;
// Records the last-modified-time of each user at the last point we saved
// the database, such that we can derive the set if users that have been
// modified since we last saved.
this._userModifiedMap = {
// user_id : timestamp
};
};
utils.inherits(IndexedDBStore, MatrixInMemoryStore);
/**
* @return {Promise} Resolved when loaded from indexed db.
*/
IndexedDBStore.prototype.startup = function() {
if (this.startedUp) {
return q();
}
return this.backend.connect().then(() => {
return this.backend.getUserPresenceEvents();
}).then((userPresenceEvents) => {
userPresenceEvents.forEach(([userId, rawEvent]) => {
const u = new User(userId);
if (rawEvent) {
u.setPresenceEvent(new MatrixEvent(rawEvent));
}
this._userModifiedMap[u.userId] = u.getLastModifiedTime();
this.storeUser(u);
});
});
};
/**
* @return {Promise} Resolves with a sync response to restore the
* client state to where it was at the last save, or null if there
* is no saved sync data.
*/
IndexedDBStore.prototype.getSavedSync = function() {
return this.backend.getSavedSync();
};
/**
* Delete all data from this store.
* @return {Promise} Resolves if the data was deleted from the database.
*/
IndexedDBStore.prototype.deleteAllData = function() {
MatrixInMemoryStore.prototype.deleteAllData.call(this);
return this.backend.clearDatabase().then(() => {
console.log("Deleted indexeddb data.");
}, (err) => {
console.error("Failed to delete indexeddb data: ", err);
throw err;
});
};
/**
* Possibly write data to the database.
* @return {Promise} Promise resolves after the write completes.
*/
IndexedDBStore.prototype.save = function() {
const now = Date.now();
if (now - this._syncTs > WRITE_DELAY_MS) {
return this._reallySave();
}
return q();
};
IndexedDBStore.prototype._reallySave = function() {
this._syncTs = Date.now(); // set now to guard against multi-writes
// work out changed users (this doesn't handle deletions but you
// can't 'delete' users as they are just presence events).
const userTuples = [];
for (const u of this.getUsers()) {
if (this._userModifiedMap[u.userId] === u.getLastModifiedTime()) continue;
if (!u.events.presence) continue;
userTuples.push([u.userId, u.events.presence.event]);
// note that we've saved this version of the user
this._userModifiedMap[u.userId] = u.getLastModifiedTime();
}
return this.backend.syncToDatabase(userTuples).catch((err) => {
console.error("sync fail:", err);
});
};
IndexedDBStore.prototype.setSyncData = function(syncData) {
return this.backend.setSyncData(syncData);
};
module.exports.IndexedDBStore = IndexedDBStore;
+67 -17
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -18,16 +19,16 @@ limitations under the License.
* This is an internal module. See {@link MatrixInMemoryStore} for the public class.
* @module store/memory
*/
var utils = require("../utils");
var User = require("../models/user");
const utils = require("../utils");
const User = require("../models/user");
const q = require("q");
/**
* Construct a new in-memory data store for the Matrix Client.
* @constructor
* @param {Object=} opts Config options
* @param {LocalStorage} opts.localStorage The local storage instance to persist
* some forms of data such as tokens. Rooms will NOT be stored. See
* {@link WebStorageStore} to persist rooms.
* some forms of data such as tokens. Rooms will NOT be stored.
*/
module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
opts = opts || {};
@@ -78,7 +79,7 @@ module.exports.MatrixInMemoryStore.prototype = {
// map up-to-date.
room.currentState.on("RoomState.members", this._onRoomMember.bind(this));
// add existing members
var self = this;
const self = this;
room.currentState.getMembers().forEach(function(m) {
self._onRoomMember(null, room.currentState, m);
});
@@ -98,12 +99,12 @@ module.exports.MatrixInMemoryStore.prototype = {
return;
}
var user = this.users[member.userId] || new User(member.userId);
const 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
member.events.member.getDirectionalContent().displayname,
);
}
}
@@ -203,7 +204,9 @@ module.exports.MatrixInMemoryStore.prototype = {
* @param {Filter} filter
*/
storeFilter: function(filter) {
if (!filter) { return; }
if (!filter) {
return;
}
if (!this.filters[filter.userId]) {
this.filters[filter.userId] = {};
}
@@ -234,8 +237,7 @@ module.exports.MatrixInMemoryStore.prototype = {
}
try {
return this.localStorage.getItem("mxjssdk_memory_filter_" + filterName);
}
catch (e) {}
} catch (e) {}
return null;
},
@@ -250,8 +252,7 @@ module.exports.MatrixInMemoryStore.prototype = {
}
try {
this.localStorage.setItem("mxjssdk_memory_filter_" + filterName, filterId);
}
catch (e) {}
} catch (e) {}
},
/**
@@ -261,7 +262,7 @@ module.exports.MatrixInMemoryStore.prototype = {
* @param {Array<MatrixEvent>} events The events to store.
*/
storeAccountDataEvents: function(events) {
var self = this;
const self = this;
events.forEach(function(event) {
self.accountData[event.getType()] = event;
});
@@ -276,9 +277,58 @@ module.exports.MatrixInMemoryStore.prototype = {
return this.accountData[eventType];
},
// TODO
//setMaxHistoryPerRoom: function(maxHistory) {},
/**
* setSyncData does nothing as there is no backing data store.
*
* @param {Object} syncData The sync data
* @return {Promise} An immediately resolved promise.
*/
setSyncData: function(syncData) {
return q();
},
// TODO
//reapOldMessages: function() {},
/**
* Save does nothing as there is no backing data store.
*/
save: function() {},
/**
* Startup does nothing as this store doesn't require starting up.
* @return {Promise} An immediately resolved promise.
*/
startup: function() {
return q();
},
/**
* @return {Promise} Resolves with a sync response to restore the
* client state to where it was at the last save, or null if there
* is no saved sync data.
*/
getSavedSync: function() {
return q(null);
},
/**
* Delete all data from this store.
* @return {Promise} An immediately resolved promise.
*/
deleteAllData: function() {
this.rooms = {
// roomId: Room
};
this.users = {
// userId: User
};
this.syncToken = null;
this.filters = {
// userId: {
// filterId: Filter
// }
};
this.accountData = {
// type : content
};
return q();
},
};
@@ -19,10 +19,10 @@ limitations under the License.
* @module store/session/webstorage
*/
var utils = require("../../utils");
const utils = require("../../utils");
var DEBUG = false; // set true to enable console logging.
var E2E_PREFIX = "session.e2e.";
const DEBUG = false; // set true to enable console logging.
const E2E_PREFIX = "session.e2e.";
/**
* Construct a web storage session store, capable of storing account keys,
@@ -37,9 +37,12 @@ function WebStorageSessionStore(webStore) {
this.store = webStore;
if (!utils.isFunction(webStore.getItem) ||
!utils.isFunction(webStore.setItem) ||
!utils.isFunction(webStore.removeItem)) {
!utils.isFunction(webStore.removeItem) ||
!utils.isFunction(webStore.key) ||
typeof(webStore.length) !== 'number'
) {
throw new Error(
"Supplied webStore does not meet the WebStorage API interface"
"Supplied webStore does not meet the WebStorage API interface",
);
}
}
@@ -92,10 +95,31 @@ WebStorageSessionStore.prototype = {
* @param {string} userId The user's ID.
* @return {object} A map from device ID to keys for the device.
*/
getEndToEndDevicesForUser: function(userId) {
getEndToEndDevicesForUser: function(userId) {
return getJsonItem(this.store, keyEndToEndDevicesForUser(userId));
},
/**
* Store the sync token corresponding to the device list.
*
* This is used when starting the client, to get a list of the users who
* have changed their device list since the list time we were running.
*
* @param {String?} token
*/
storeEndToEndDeviceSyncToken: function(token) {
setJsonItem(this.store, KEY_END_TO_END_DEVICE_SYNC_TOKEN, token);
},
/**
* Get the sync token corresponding to the device list.
*
* @return {String?} token
*/
getEndToEndDeviceSyncToken: function() {
return getJsonItem(this.store, KEY_END_TO_END_DEVICE_SYNC_TOKEN);
},
/**
* Store a session between the logged-in user and another device
* @param {string} deviceKey The public key of the other device.
@@ -103,10 +127,10 @@ WebStorageSessionStore.prototype = {
* @param {string} session Base64 encoded end-to-end session.
*/
storeEndToEndSession: function(deviceKey, sessionId, session) {
var sessions = this.getEndToEndSessions(deviceKey) || {};
const sessions = this.getEndToEndSessions(deviceKey) || {};
sessions[sessionId] = session;
setJsonItem(
this.store, keyEndToEndSessions(deviceKey), sessions
this.store, keyEndToEndSessions(deviceKey), sessions,
);
},
@@ -120,13 +144,39 @@ WebStorageSessionStore.prototype = {
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
},
/**
* Retrieve a list of all known inbound group sessions
*
* @return {{senderKey: string, sessionId: string}}
*/
getAllEndToEndInboundGroupSessionKeys: function() {
const prefix = E2E_PREFIX + 'inboundgroupsessions/';
const result = [];
for (let i = 0; i < this.store.length; i++) {
const key = this.store.key(i);
if (!key.startsWith(prefix)) {
continue;
}
// we can't use split, as the components we are trying to split out
// might themselves contain '/' characters. We rely on the
// senderKey being a (32-byte) curve25519 key, base64-encoded
// (hence 43 characters long).
result.push({
senderKey: key.substr(prefix.length, 43),
sessionId: key.substr(prefix.length + 44),
});
}
return result;
},
getEndToEndInboundGroupSession: function(senderKey, sessionId) {
var key = keyEndToEndInboundGroupSession(senderKey, sessionId);
const key = keyEndToEndInboundGroupSession(senderKey, sessionId);
return this.store.getItem(key);
},
storeEndToEndInboundGroupSession: function(senderKey, sessionId, pickledSession) {
var key = keyEndToEndInboundGroupSession(senderKey, sessionId);
const key = keyEndToEndInboundGroupSession(senderKey, sessionId);
return this.store.setItem(key, pickledSession);
},
@@ -146,11 +196,12 @@ WebStorageSessionStore.prototype = {
*/
getEndToEndRoom: function(roomId) {
return getJsonItem(this.store, keyEndToEndRoom(roomId));
}
},
};
var KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
var KEY_END_TO_END_ANNOUNCED = E2E_PREFIX + "announced";
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
const KEY_END_TO_END_ANNOUNCED = E2E_PREFIX + "announced";
const KEY_END_TO_END_DEVICE_SYNC_TOKEN = E2E_PREFIX + "device_sync_token";
function keyEndToEndDevicesForUser(userId) {
return E2E_PREFIX + "devices/" + userId;
@@ -170,9 +221,10 @@ function keyEndToEndRoom(roomId) {
function getJsonItem(store, key) {
try {
// if the key is absent, store.getItem() returns null, and
// JSON.parse(null) === null, so this returns null.
return JSON.parse(store.getItem(key));
}
catch (e) {
} catch (e) {
debuglog("Failed to get key %s: %s", key, e);
debuglog(e.stack);
}
@@ -185,7 +237,7 @@ function setJsonItem(store, key, val) {
function debuglog() {
if (DEBUG) {
console.log.apply(console, arguments);
console.log(...arguments);
}
}
+41 -4
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,6 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import q from "q";
/**
* This is an internal module.
* @module store/stub
@@ -180,11 +182,46 @@ StubStore.prototype = {
},
// TODO
//setMaxHistoryPerRoom: function(maxHistory) {},
/**
* setSyncData does nothing as there is no backing data store.
*
* @param {Object} syncData The sync data
* @return {Promise} An immediately resolved promise.
*/
setSyncData: function(syncData) {
return q();
},
// TODO
//reapOldMessages: function() {},
/**
* Save does nothing as there is no backing data store.
*/
save: function() {},
/**
* Startup does nothing.
* @return {Promise} An immediately resolved promise.
*/
startup: function() {
return q();
},
/**
* @return {Promise} Resolves with a sync response to restore the
* client state to where it was at the last save, or null if there
* is no saved sync data.
*/
getSavedSync: function() {
return q(null);
},
/**
* Delete all data from this store. Does nothing since this store
* doesn't store anything.
* @return {Promise} An immediately resolved promise.
*/
deleteAllData: function() {
return q();
},
};
/** Stub Store class. */
+488
View File
@@ -0,0 +1,488 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* This is an internal module. See {@link SyncAccumulator} for the public class.
* @module sync-accumulator
*/
import utils from "./utils";
/**
* The purpose of this class is to accumulate /sync responses such that a
* complete "initial" JSON response can be returned which accurately represents
* the sum total of the /sync responses accumulated to date. It only handles
* room data: that is, everything under the "rooms" top-level key.
*
* This class is used when persisting room data so a complete /sync response can
* be loaded from disk and incremental syncs can be performed on the server,
* rather than asking the server to do an initial sync on startup.
*/
class SyncAccumulator {
/**
* @param {Object} opts
* @param {Number=} opts.maxTimelineEntries The ideal maximum number of
* timeline entries to keep in the sync response. This is best-effort, as
* clients do not always have a back-pagination token for each event, so
* it's possible there may be slightly *less* than this value. There will
* never be more. This cannot be 0 or else it makes it impossible to scroll
* back in a room. Default: 50.
*/
constructor(opts) {
opts = opts || {};
opts.maxTimelineEntries = opts.maxTimelineEntries || 50;
this.opts = opts;
this.accountData = {
//$event_type: Object
};
this.inviteRooms = {
//$roomId: { ... sync 'invite' json data ... }
};
this.joinRooms = {
//$roomId: {
// _currentState: { $event_type: { $state_key: json } },
// _timeline: [
// { event: $event, token: null|token },
// { event: $event, token: null|token },
// { event: $event, token: null|token },
// ...
// ],
// _accountData: { $event_type: json },
// _unreadNotifications: { ... unread_notifications JSON ... },
// _readReceipts: { $user_id: { data: $json, eventId: $event_id }}
//}
};
// the /sync token which corresponds to the last time rooms were
// accumulated. We remember this so that any caller can obtain a
// coherent /sync response and know at what point they should be
// streaming from without losing events.
this.nextBatch = null;
}
accumulate(syncResponse) {
this._accumulateRooms(syncResponse);
this._accumulateAccountData(syncResponse);
this.nextBatch = syncResponse.next_batch;
}
_accumulateAccountData(syncResponse) {
if (!syncResponse.account_data || !syncResponse.account_data.events) {
return;
}
// Clobbers based on event type.
syncResponse.account_data.events.forEach((e) => {
this.accountData[e.type] = e;
});
}
/**
* Accumulate incremental /sync room data.
* @param {Object} syncResponse the complete /sync JSON
*/
_accumulateRooms(syncResponse) {
if (!syncResponse.rooms) {
return;
}
if (syncResponse.rooms.invite) {
Object.keys(syncResponse.rooms.invite).forEach((roomId) => {
this._accumulateRoom(
roomId, "invite", syncResponse.rooms.invite[roomId],
);
});
}
if (syncResponse.rooms.join) {
Object.keys(syncResponse.rooms.join).forEach((roomId) => {
this._accumulateRoom(
roomId, "join", syncResponse.rooms.join[roomId],
);
});
}
if (syncResponse.rooms.leave) {
Object.keys(syncResponse.rooms.leave).forEach((roomId) => {
this._accumulateRoom(
roomId, "leave", syncResponse.rooms.leave[roomId],
);
});
}
}
_accumulateRoom(roomId, category, data) {
// Valid /sync state transitions
// +--------+ <======+ 1: Accept an invite
// +== | INVITE | | (5) 2: Leave a room
// | +--------+ =====+ | 3: Join a public room previously
// |(1) (4) | | left (handle as if new room)
// V (2) V | 4: Reject an invite
// +------+ ========> +--------+ 5: Invite to a room previously
// | JOIN | (3) | LEAVE* | left (handle as if new room)
// +------+ <======== +--------+
//
// * equivalent to "no state"
switch (category) {
case "invite": // (5)
this._accumulateInviteState(roomId, data);
break;
case "join":
if (this.inviteRooms[roomId]) { // (1)
// was previously invite, now join. We expect /sync to give
// the entire state and timeline on 'join', so delete previous
// invite state
delete this.inviteRooms[roomId];
}
// (3)
this._accumulateJoinState(roomId, data);
break;
case "leave":
if (this.inviteRooms[roomId]) { // (4)
delete this.inviteRooms[roomId];
} else { // (2)
delete this.joinRooms[roomId];
}
break;
default:
console.error("Unknown cateogory: ", category);
}
}
_accumulateInviteState(roomId, data) {
if (!data.invite_state || !data.invite_state.events) { // no new data
return;
}
if (!this.inviteRooms[roomId]) {
this.inviteRooms[roomId] = {
invite_state: data.invite_state,
};
return;
}
// accumulate extra keys for invite->invite transitions
// clobber based on event type / state key
// We expect invite_state to be small, so just loop over the events
const currentData = this.inviteRooms[roomId];
data.invite_state.events.forEach((e) => {
let hasAdded = false;
for (let i = 0; i < currentData.invite_state.events.length; i++) {
const current = currentData.invite_state.events[i];
if (current.type === e.type && current.state_key == e.state_key) {
currentData.invite_state.events[i] = e; // update
hasAdded = true;
}
}
if (!hasAdded) {
currentData.invite_state.events.push(e);
}
});
}
// Accumulate timeline and state events in a room.
_accumulateJoinState(roomId, data) {
// We expect this function to be called a lot (every /sync) so we want
// this to be fast. /sync stores events in an array but we often want
// to clobber based on type/state_key. Rather than convert arrays to
// maps all the time, just keep private maps which contain
// the actual current accumulated sync state, and array-ify it when
// getJSON() is called.
// State resolution:
// The 'state' key is the delta from the previous sync (or start of time
// if no token was supplied), to the START of the timeline. To obtain
// the current state, we need to "roll forward" state by reading the
// timeline. We want to store the current state so we can drop events
// out the end of the timeline based on opts.maxTimelineEntries.
//
// 'state' 'timeline' current state
// |-------x<======================>x
// T I M E
//
// When getJSON() is called, we 'roll back' the current state by the
// number of entries in the timeline to work out what 'state' should be.
// Back-pagination:
// On an initial /sync, the server provides a back-pagination token for
// the start of the timeline. When /sync deltas come down, they also
// include back-pagination tokens for the start of the timeline. This
// means not all events in the timeline have back-pagination tokens, as
// it is only the ones at the START of the timeline which have them.
// In order for us to have a valid timeline (and back-pagination token
// to match), we need to make sure that when we remove old timeline
// events, that we roll forward to an event which has a back-pagination
// token. This means we can't keep a strict sliding-window based on
// opts.maxTimelineEntries, and we may have a few less. We should never
// have more though, provided that the /sync limit is less than or equal
// to opts.maxTimelineEntries.
if (!this.joinRooms[roomId]) {
// Create truly empty objects so event types of 'hasOwnProperty' and co
// don't cause this code to break.
this.joinRooms[roomId] = {
_currentState: Object.create(null),
_timeline: [],
_accountData: Object.create(null),
_unreadNotifications: {},
_readReceipts: {},
};
}
const currentData = this.joinRooms[roomId];
if (data.account_data && data.account_data.events) {
// clobber based on type
data.account_data.events.forEach((e) => {
currentData._accountData[e.type] = e;
});
}
// these probably clobber, spec is unclear.
if (data.unread_notifications) {
currentData._unreadNotifications = data.unread_notifications;
}
if (data.ephemeral && data.ephemeral.events) {
data.ephemeral.events.forEach((e) => {
// We purposefully do not persist m.typing events.
// Technically you could refresh a browser before the timer on a
// typing event is up, so it'll look like you aren't typing when
// you really still are. However, the alternative is worse. If
// we do persist typing events, it will look like people are
// typing forever until someone really does start typing (which
// will prompt Synapse to send down an actual m.typing event to
// clobber the one we persisted).
if (e.type !== "m.receipt" || !e.content) {
// This means we'll drop unknown ephemeral events but that
// seems okay.
return;
}
// Handle m.receipt events. They clobber based on:
// (user_id, receipt_type)
// but they are keyed in the event as:
// content:{ $event_id: { $receipt_type: { $user_id: {json} }}}
// so store them in the former so we can accumulate receipt deltas
// quickly and efficiently (we expect a lot of them). Fold the
// receipt type into the key name since we only have 1 at the
// moment (m.read) and nested JSON objects are slower and more
// of a hassle to work with. We'll inflate this back out when
// getJSON() is called.
Object.keys(e.content).forEach((eventId) => {
if (!e.content[eventId]["m.read"]) {
return;
}
Object.keys(e.content[eventId]["m.read"]).forEach((userId) => {
// clobber on user ID
currentData._readReceipts[userId] = {
data: e.content[eventId]["m.read"][userId],
eventId: eventId,
};
});
});
});
}
// if we got a limited sync, we need to remove all timeline entries or else
// we will have gaps in the timeline.
if (data.timeline && data.timeline.limited) {
currentData._timeline = [];
}
// Work out the current state. The deltas need to be applied in the order:
// - existing state which didn't come down /sync.
// - State events under the 'state' key.
// - State events in the 'timeline'.
if (data.state && data.state.events) {
data.state.events.forEach((e) => {
setState(currentData._currentState, e);
});
}
if (data.timeline && data.timeline.events) {
data.timeline.events.forEach((e, index) => {
// this nops if 'e' isn't a state event
setState(currentData._currentState, e);
// append the event to the timeline. The back-pagination token
// corresponds to the first event in the timeline
currentData._timeline.push({
event: e,
token: index === 0 ? data.timeline.prev_batch : null,
});
});
}
// attempt to prune the timeline by jumping between events which have
// pagination tokens.
if (currentData._timeline.length > this.opts.maxTimelineEntries) {
const startIndex = (
currentData._timeline.length - this.opts.maxTimelineEntries
);
for (let i = startIndex; i < currentData._timeline.length; i++) {
if (currentData._timeline[i].token) {
// keep all events after this, including this one
currentData._timeline = currentData._timeline.slice(
i, currentData._timeline.length,
);
break;
}
}
}
}
/**
* Return everything under the 'rooms' key from a /sync response which
* represents all room data that should be stored. This should be paired
* with the sync token which represents the most recent /sync response
* provided to accumulate().
* @return {Object} An object with a "nextBatch", "roomsData" and "accountData"
* keys.
* The "nextBatch" key is a string which represents at what point in the
* /sync stream the accumulator reached. This token should be used when
* restarting a /sync stream at startup. Failure to do so can lead to missing
* events. The "roomsData" key is an Object which represents the entire
* /sync response from the 'rooms' key onwards. The "accountData" key is
* a list of raw events which represent global account data.
*/
getJSON() {
const data = {
join: {},
invite: {},
// always empty. This is set by /sync when a room was previously
// in 'invite' or 'join'. On fresh startup, the client won't know
// about any previous room being in 'invite' or 'join' so we can
// just omit mentioning it at all, even if it has previously come
// down /sync.
// The notable exception is when a client is kicked or banned:
// we may want to hold onto that room so the client can clearly see
// why their room has disappeared. We don't persist it though because
// it is unclear *when* we can safely remove the room from the DB.
// Instead, we assume that if you're loading from the DB, you've
// refreshed the page, which means you've seen the kick/ban already.
leave: {},
};
Object.keys(this.inviteRooms).forEach((roomId) => {
data.invite[roomId] = this.inviteRooms[roomId];
});
Object.keys(this.joinRooms).forEach((roomId) => {
const roomData = this.joinRooms[roomId];
const roomJson = {
ephemeral: { events: [] },
account_data: { events: [] },
state: { events: [] },
timeline: {
events: [],
prev_batch: null,
},
unread_notifications: roomData._unreadNotifications,
};
// Add account data
Object.keys(roomData._accountData).forEach((evType) => {
roomJson.account_data.events.push(roomData._accountData[evType]);
});
// Add receipt data
const receiptEvent = {
type: "m.receipt",
room_id: roomId,
content: {
// $event_id: { "m.read": { $user_id: $json } }
},
};
Object.keys(roomData._readReceipts).forEach((userId) => {
const receiptData = roomData._readReceipts[userId];
if (!receiptEvent.content[receiptData.eventId]) {
receiptEvent.content[receiptData.eventId] = {
"m.read": {},
};
}
receiptEvent.content[receiptData.eventId]["m.read"][userId] = (
receiptData.data
);
});
// add only if we have some receipt data
if (Object.keys(receiptEvent.content).length > 0) {
roomJson.ephemeral.events.push(receiptEvent);
}
// Add timeline data
roomData._timeline.forEach((msgData) => {
if (!roomJson.timeline.prev_batch) {
// the first event we add to the timeline MUST match up to
// the prev_batch token.
if (!msgData.token) {
return; // this shouldn't happen as we prune constantly.
}
roomJson.timeline.prev_batch = msgData.token;
}
roomJson.timeline.events.push(msgData.event);
});
// Add state data: roll back current state to the start of timeline,
// by "reverse clobbering" from the end of the timeline to the start.
// Convert maps back into arrays.
const rollBackState = Object.create(null);
for (let i = roomJson.timeline.events.length - 1; i >=0; i--) {
const timelineEvent = roomJson.timeline.events[i];
if (timelineEvent.state_key === null ||
timelineEvent.state_key === undefined) {
continue; // not a state event
}
// since we're going back in time, we need to use the previous
// state value else we'll break causality. We don't have the
// complete previous state event, so we need to create one.
const prevStateEvent = utils.deepCopy(timelineEvent);
if (prevStateEvent.unsigned) {
if (prevStateEvent.unsigned.prev_content) {
prevStateEvent.content = prevStateEvent.unsigned.prev_content;
}
if (prevStateEvent.unsigned.prev_sender) {
prevStateEvent.sender = prevStateEvent.unsigned.prev_sender;
}
}
setState(rollBackState, prevStateEvent);
}
Object.keys(roomData._currentState).forEach((evType) => {
Object.keys(roomData._currentState[evType]).forEach((stateKey) => {
let ev = roomData._currentState[evType][stateKey];
if (rollBackState[evType] && rollBackState[evType][stateKey]) {
// use the reverse clobbered event instead.
ev = rollBackState[evType][stateKey];
}
roomJson.state.events.push(ev);
});
});
data.join[roomId] = roomJson;
});
// Add account data
const accData = [];
Object.keys(this.accountData).forEach((evType) => {
accData.push(this.accountData[evType]);
});
return {
nextBatch: this.nextBatch,
roomsData: data,
accountData: accData,
};
}
}
function setState(eventMap, event) {
if (event.state_key === null || event.state_key === undefined || !event.type) {
return;
}
if (!eventMap[event.type]) {
eventMap[event.type] = Object.create(null);
}
eventMap[event.type][event.state_key] = event;
}
module.exports = SyncAccumulator;
+294 -158
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -23,20 +24,25 @@ limitations under the License.
* an alternative syncing API, we may want to have a proper syncing interface
* for HTTP and WS at some point.
*/
var q = require("q");
var User = require("./models/user");
var Room = require("./models/room");
var utils = require("./utils");
var Filter = require("./filter");
var EventTimeline = require("./models/event-timeline");
const q = require("q");
const User = require("./models/user");
const Room = require("./models/room");
const utils = require("./utils");
const Filter = require("./filter");
const EventTimeline = require("./models/event-timeline");
var DEBUG = true;
const DEBUG = true;
// /sync requests allow you to set a timeout= but the request may continue
// beyond that and wedge forever, so we need to track how long we are willing
// to keep open the connection. This constant is *ADDED* to the timeout= value
// to determine the max time we're willing to wait.
var BUFFER_PERIOD_MS = 80 * 1000;
const BUFFER_PERIOD_MS = 80 * 1000;
// Number of consecutive failed syncs that will lead to a syncState of ERROR as opposed
// to RECONNECTING. This is needed to inform the client of server issues when the
// keepAlive is successful but the server /sync fails.
const FAILED_SYNC_ERROR_THRESHOLD = 3;
function getFilterName(userId, suffix) {
// scope this on the user ID because people may login on many accounts
@@ -45,8 +51,10 @@ function getFilterName(userId, suffix) {
}
function debuglog() {
if (!DEBUG) { return; }
console.log.apply(console, arguments);
if (!DEBUG) {
return;
}
console.log(...arguments);
}
@@ -56,6 +64,12 @@ function debuglog() {
* @constructor
* @param {MatrixClient} client The matrix client instance to use.
* @param {Object} opts Config options
* @param {module:crypto=} opts.crypto Crypto manager
* @param {Function=} opts.canResetEntireTimeline A function which is called
* with a room ID and returns a boolean. It should return 'true' if the SDK can
* SAFELY remove events from this room. It may not be safe to remove events if
* there are other references to the timelines for this room.
* Default: returns false.
*/
function SyncApi(client, opts) {
this.client = client;
@@ -66,14 +80,21 @@ function SyncApi(client, opts) {
opts.resolveInvitesToProfiles = opts.resolveInvitesToProfiles || false;
opts.pollTimeout = opts.pollTimeout || (30 * 1000);
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
if (!opts.canResetEntireTimeline) {
opts.canResetEntireTimeline = function(roomId) {
return false;
};
}
this.opts = opts;
this._peekRoomId = null;
this._currentSyncRequest = null;
this._syncState = null;
this._catchingUp = false;
this._running = false;
this._keepAliveTimer = null;
this._connectionReturnedDefer = null;
this._notifEvents = []; // accumulator of sync events in the current sync response
this._failedSyncCount = 0; // Number of consecutive failed /sync requests
if (client.getNotifTimelineSet()) {
reEmit(client, client.getNotifTimelineSet(),
@@ -86,8 +107,8 @@ function SyncApi(client, opts) {
* @return {Room}
*/
SyncApi.prototype.createRoom = function(roomId) {
var client = this.client;
var room = new Room(roomId, {
const client = this.client;
const room = new Room(roomId, {
pendingEventOrdering: this.opts.pendingEventOrdering,
timelineSupport: client.timelineSupport,
});
@@ -106,12 +127,12 @@ SyncApi.prototype.createRoom = function(roomId) {
* @private
*/
SyncApi.prototype._registerStateListeners = function(room) {
var client = this.client;
const client = this.client;
// we need to also re-emit room state and room member events, so hook it up
// to the client now. We need to add a listener for RoomState.members in
// order to hook them correctly. (TODO: find a better way?)
reEmit(client, room.currentState, [
"RoomState.events", "RoomState.members", "RoomState.newMember"
"RoomState.events", "RoomState.members", "RoomState.newMember",
]);
room.currentState.on("RoomState.newMember", function(event, state, member) {
member.user = client.getUser(member.userId);
@@ -119,8 +140,8 @@ SyncApi.prototype._registerStateListeners = function(room) {
client, member,
[
"RoomMember.name", "RoomMember.typing", "RoomMember.powerLevel",
"RoomMember.membership"
]
"RoomMember.membership",
],
);
});
};
@@ -142,34 +163,34 @@ SyncApi.prototype._deregisterStateListeners = function(room) {
* @return {Promise} Resolved when they've been added to the store.
*/
SyncApi.prototype.syncLeftRooms = function() {
var client = this.client;
var self = this;
const client = this.client;
const self = this;
// grab a filter with limit=1 and include_leave=true
var filter = new Filter(this.client.credentials.userId);
const filter = new Filter(this.client.credentials.userId);
filter.setTimelineLimit(1);
filter.setIncludeLeaveRooms(true);
var localTimeoutMs = this.opts.pollTimeout + BUFFER_PERIOD_MS;
var qps = {
timeout: 0 // don't want to block since this is a single isolated req
const localTimeoutMs = this.opts.pollTimeout + BUFFER_PERIOD_MS;
const qps = {
timeout: 0, // don't want to block since this is a single isolated req
};
return client.getOrCreateFilter(
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter
getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter,
).then(function(filterId) {
qps.filter = filterId;
return client._http.authedRequest(
undefined, "GET", "/sync", qps, undefined, localTimeoutMs
undefined, "GET", "/sync", qps, undefined, localTimeoutMs,
);
}).then(function(data) {
var leaveRooms = [];
let leaveRooms = [];
if (data.rooms && data.rooms.leave) {
leaveRooms = self._mapSyncResponseToRoomArray(data.rooms.leave);
}
var rooms = [];
const rooms = [];
leaveRooms.forEach(function(leaveObj) {
var room = leaveObj.room;
const room = leaveObj.room;
rooms.push(room);
if (!leaveObj.isBrandNewRoom) {
// the intention behind syncLeftRooms is to add in rooms which were
@@ -183,9 +204,9 @@ SyncApi.prototype.syncLeftRooms = function() {
return;
}
leaveObj.timeline = leaveObj.timeline || {};
var timelineEvents =
const timelineEvents =
self._mapSyncEventsFormat(leaveObj.timeline, room);
var stateEvents = self._mapSyncEventsFormat(leaveObj.state, room);
const stateEvents = self._mapSyncEventsFormat(leaveObj.state, room);
// set the back-pagination token. Do this *before* adding any
// events so that clients can start back-paginating.
@@ -210,8 +231,8 @@ SyncApi.prototype.syncLeftRooms = function() {
* store.
*/
SyncApi.prototype.peek = function(roomId) {
var self = this;
var client = this.client;
const self = this;
const client = this.client;
this._peekRoomId = roomId;
return this.client.roomInitialSync(roomId, 20).then(function(response) {
// make sure things are init'd
@@ -219,18 +240,18 @@ SyncApi.prototype.peek = function(roomId) {
response.messages.chunk = response.messages.chunk || [];
response.state = response.state || [];
var peekRoom = self.createRoom(roomId);
const peekRoom = self.createRoom(roomId);
// FIXME: Mostly duplicated from _processRoomEvents but not entirely
// because "state" in this API is at the BEGINNING of the chunk
var oldStateEvents = utils.map(
utils.deepCopy(response.state), client.getEventMapper()
const oldStateEvents = utils.map(
utils.deepCopy(response.state), client.getEventMapper(),
);
var stateEvents = utils.map(
response.state, client.getEventMapper()
const stateEvents = utils.map(
response.state, client.getEventMapper(),
);
var messages = utils.map(
response.messages.chunk, client.getEventMapper()
const messages = utils.map(
response.messages.chunk, client.getEventMapper(),
);
// XXX: copypasted from /sync until we kill off this
@@ -239,11 +260,10 @@ SyncApi.prototype.peek = function(roomId) {
if (response.presence && utils.isArray(response.presence)) {
response.presence.map(client.getEventMapper()).forEach(
function(presenceEvent) {
var user = client.store.getUser(presenceEvent.getContent().user_id);
let user = client.store.getUser(presenceEvent.getContent().user_id);
if (user) {
user.setPresenceEvent(presenceEvent);
}
else {
} else {
user = createNewUser(client, presenceEvent.getContent().user_id);
user.setPresenceEvent(presenceEvent);
client.store.storeUser(user);
@@ -300,14 +320,13 @@ SyncApi.prototype._peekPoll = function(roomId, token) {
return;
}
var self = this;
const self = this;
// FIXME: gut wrenching; hard-coded timeout values
this.client._http.authedRequest(undefined, "GET", "/events", {
room_id: roomId,
timeout: 30 * 1000,
from: token
from: token,
}, undefined, 50 * 1000).done(function(res) {
// We have a problem that we get presence both from /events and /sync
// however, /sync only returns presence for users in rooms
// you're actually joined to.
@@ -320,11 +339,10 @@ SyncApi.prototype._peekPoll = function(roomId, token) {
res.chunk.filter(function(e) {
return e.type === "m.presence";
}).map(self.client.getEventMapper()).forEach(function(presenceEvent) {
var user = self.client.store.getUser(presenceEvent.getContent().user_id);
let user = self.client.store.getUser(presenceEvent.getContent().user_id);
if (user) {
user.setPresenceEvent(presenceEvent);
}
else {
} else {
user = createNewUser(self.client, presenceEvent.getContent().user_id);
user.setPresenceEvent(presenceEvent);
self.client.store.storeUser(user);
@@ -333,10 +351,10 @@ SyncApi.prototype._peekPoll = function(roomId, token) {
});
// strip out events which aren't for the given room_id (e.g presence)
var events = res.chunk.filter(function(e) {
const events = res.chunk.filter(function(e) {
return e.room_id === roomId;
}).map(self.client.getEventMapper());
var room = self.client.getRoom(roomId);
const room = self.client.getRoom(roomId);
room.addLiveEvents(events);
self._peekPoll(roomId, res.end);
}, function(err) {
@@ -360,11 +378,8 @@ SyncApi.prototype.getSyncState = function() {
* Main entry point
*/
SyncApi.prototype.sync = function() {
debuglog("SyncApi.sync: starting with sync token " +
this.client.store.getSyncToken());
var client = this.client;
var self = this;
const client = this.client;
const self = this;
this._running = true;
@@ -393,7 +408,7 @@ SyncApi.prototype.sync = function() {
}
function getFilter() {
var filter;
let filter;
if (self.opts.filter) {
filter = self.opts.filter;
} else {
@@ -402,7 +417,7 @@ SyncApi.prototype.sync = function() {
}
client.getOrCreateFilter(
getFilterName(client.credentials.userId), filter
getFilterName(client.credentials.userId), filter,
).done(function(filterId) {
// reset the notifications timeline to prepare it to paginate from
// the current point in time.
@@ -422,8 +437,7 @@ SyncApi.prototype.sync = function() {
if (client.isGuest()) {
// no push rules for guests, no access to POST filter for guests.
self._sync({});
}
else {
} else {
getPushRules();
}
};
@@ -438,7 +452,9 @@ SyncApi.prototype.stop = function() {
this._onOnlineBound = undefined;
}
this._running = false;
if (this._currentSyncRequest) { this._currentSyncRequest.abort(); }
if (this._currentSyncRequest) {
this._currentSyncRequest.abort();
}
if (this._keepAliveTimer) {
clearTimeout(this._keepAliveTimer);
this._keepAliveTimer = null;
@@ -451,7 +467,9 @@ SyncApi.prototype.stop = function() {
* @return {boolean} True if this resulted in a request being retried.
*/
SyncApi.prototype.retryImmediately = function() {
if (!this._connectionReturnedDefer) { return false; }
if (!this._connectionReturnedDefer) {
return false;
}
this._startKeepAlives(0);
return true;
};
@@ -463,8 +481,8 @@ SyncApi.prototype.retryImmediately = function() {
* @param {boolean} syncOptions.hasSyncedBefore
*/
SyncApi.prototype._sync = function(syncOptions) {
var client = this.client;
var self = this;
const client = this.client;
const self = this;
if (!this._running) {
debuglog("Sync no longer running: exiting.");
@@ -476,16 +494,37 @@ SyncApi.prototype._sync = function(syncOptions) {
return;
}
var filterId = syncOptions.filterId;
let filterId = syncOptions.filterId;
if (client.isGuest() && !filterId) {
filterId = this._getGuestFilter();
}
var syncToken = client.store.getSyncToken();
const syncToken = client.store.getSyncToken();
var qps = {
let pollTimeout = this.opts.pollTimeout;
if (this.getSyncState() !== 'SYNCING' || this._catchingUp) {
// unless we are happily syncing already, we want the server to return
// as quickly as possible, even if there are no events queued. This
// serves two purposes:
//
// * When the connection dies, we want to know asap when it comes back,
// so that we can hide the error from the user. (We don't want to
// have to wait for an event or a timeout).
//
// * We want to know if the server has any to_device messages queued up
// for us. We do that by calling it with a zero timeout until it
// doesn't give us any more to_device messages.
this._catchingUp = true;
pollTimeout = 0;
}
// normal timeout= plus buffer time
const clientSideTimeoutMs = pollTimeout + BUFFER_PERIOD_MS;
const qps = {
filter: filterId,
timeout: this.opts.pollTimeout,
timeout: pollTimeout,
};
if (syncToken) {
@@ -505,37 +544,91 @@ SyncApi.prototype._sync = function(syncOptions) {
qps.timeout = 0;
}
// normal timeout= plus buffer time
var clientSideTimeoutMs = this.opts.pollTimeout + BUFFER_PERIOD_MS;
let isCachedResponse = false;
this._currentSyncRequest = client._http.authedRequest(
undefined, "GET", "/sync", qps, undefined, clientSideTimeoutMs
);
let syncPromise;
if (!syncOptions.hasSyncedBefore) {
// Don't do an HTTP hit to /sync. Instead, load up the persisted /sync data,
// if there is data there.
syncPromise = client.store.getSavedSync();
} else {
syncPromise = q(null);
}
syncPromise.then((savedSync) => {
if (savedSync) {
debuglog("sync(): not doing HTTP hit, instead returning stored /sync data");
isCachedResponse = true;
return {
next_batch: savedSync.nextBatch,
rooms: savedSync.roomsData,
account_data: {
events: savedSync.accountData,
},
};
} else {
//debuglog('Starting sync since=' + syncToken);
this._currentSyncRequest = client._http.authedRequest(
undefined, "GET", "/sync", qps, undefined, clientSideTimeoutMs,
);
return this._currentSyncRequest;
}
}).then(function(data) {
//debuglog('Completed sync, next_batch=' + data.next_batch);
this._currentSyncRequest.done(function(data) {
// set the sync token NOW *before* processing the events. We do this so
// if something barfs on an event we can skip it rather than constantly
// polling with the same token.
client.store.setSyncToken(data.next_batch);
// Reset after a successful sync
self._failedSyncCount = 0;
// We need to wait until the sync data has been sent to the backend
// because it appears that the sync data gets modified somewhere in
// processing it in such a way as to make it no longer cloneable.
// XXX: Find out what is modifying it!
if (!isCachedResponse) {
// Don't give the store back its own cached data
return client.store.setSyncData(data).then(() => {
return data;
});
} else {
return q(data);
}
}).done((data) => {
try {
self._processSyncResponse(syncToken, data);
}
catch (e) {
} catch (e) {
// log the exception with stack if we have it, else fall back
// to the plain description
console.error("Caught /sync error", e.stack || e);
}
// emit synced events
const syncEventData = {
oldSyncToken: syncToken,
nextSyncToken: data.next_batch,
catchingUp: self._catchingUp,
};
if (!syncOptions.hasSyncedBefore) {
self._updateSyncState("PREPARED");
self._updateSyncState("PREPARED", syncEventData);
syncOptions.hasSyncedBefore = true;
}
// keep emitting SYNCING -> SYNCING for clients who want to do bulk updates
self._updateSyncState("SYNCING");
if (!isCachedResponse) {
self._updateSyncState("SYNCING", syncEventData);
// tell databases that everything is now in a consistent state and can be
// saved (no point doing so if we only have the data we just got out of the
// store).
client.store.save();
}
// Begin next sync
self._sync(syncOptions);
}, function(err) {
if (!self._running) {
@@ -550,6 +643,9 @@ SyncApi.prototype._sync = function(syncOptions) {
console.error("/sync error %s", err);
console.error(err);
self._failedSyncCount++;
console.log('Number of consecutive failed sync requests:', self._failedSyncCount);
debuglog("Starting keep-alive");
// Note that we do *not* mark the sync connection as
// lost yet: we only do this if a keepalive poke
@@ -562,7 +658,11 @@ SyncApi.prototype._sync = function(syncOptions) {
self._sync(syncOptions);
});
self._currentSyncRequest = null;
self._updateSyncState("RECONNECTING");
// Transition from RECONNECTING to ERROR after a given number of failed syncs
self._updateSyncState(
self._failedSyncCount >= FAILED_SYNC_ERROR_THRESHOLD ?
"ERROR" : "RECONNECTING",
);
});
};
@@ -575,14 +675,15 @@ SyncApi.prototype._sync = function(syncOptions) {
* @param {Object} data The response from /sync
*/
SyncApi.prototype._processSyncResponse = function(syncToken, data) {
var client = this.client;
var self = this;
const client = this.client;
const self = this;
// data looks like:
// {
// next_batch: $token,
// presence: { events: [] },
// account_data: { events: [] },
// device_lists: { changed: ["@user:server", ... ]},
// to_device: { events: [] },
// rooms: {
// invite: {
@@ -620,11 +721,10 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
if (data.presence && utils.isArray(data.presence.events)) {
data.presence.events.map(client.getEventMapper()).forEach(
function(presenceEvent) {
var user = client.store.getUser(presenceEvent.getSender());
let user = client.store.getUser(presenceEvent.getSender());
if (user) {
user.setPresenceEvent(presenceEvent);
}
else {
} else {
user = createNewUser(client, presenceEvent.getSender());
user.setPresenceEvent(presenceEvent);
client.store.storeUser(user);
@@ -635,7 +735,7 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
// handle non-room account_data
if (data.account_data && utils.isArray(data.account_data.events)) {
var events = data.account_data.events.map(client.getEventMapper());
const events = data.account_data.events.map(client.getEventMapper());
client.store.storeAccountDataEvents(events);
events.forEach(
function(accountDataEvent) {
@@ -644,38 +744,45 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
}
client.emit("accountData", accountDataEvent);
return accountDataEvent;
}
},
);
}
// handle to-device events
if (data.to_device && utils.isArray(data.to_device.events)) {
if (data.to_device && utils.isArray(data.to_device.events) &&
data.to_device.events.length > 0
) {
data.to_device.events
.map(client.getEventMapper())
.forEach(
function(toDeviceEvent) {
var content = toDeviceEvent.getContent();
const content = toDeviceEvent.getContent();
if (
toDeviceEvent.getType() == "m.room.message" &&
content.msgtype == "m.bad.encrypted"
) {
console.warn(
"Unable to decrypt to-device event: " + content.body
// the mapper already logged a warning.
console.log(
'Ignoring undecryptable to-device event from ' +
toDeviceEvent.getSender(),
);
return;
}
client.emit("toDeviceEvent", toDeviceEvent);
}
},
);
} else {
// no more to-device events: we can stop polling with a short timeout.
this._catchingUp = false;
}
// 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 = [];
var joinRooms = [];
var leaveRooms = [];
let inviteRooms = [];
let joinRooms = [];
let leaveRooms = [];
if (data.rooms) {
if (data.rooms.invite) {
@@ -693,8 +800,8 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
// Handle invites
inviteRooms.forEach(function(inviteObj) {
var room = inviteObj.room;
var stateEvents =
const room = inviteObj.room;
const stateEvents =
self._mapSyncEventsFormat(inviteObj.invite_state, room);
self._processRoomEvents(room, stateEvents);
if (inviteObj.isBrandNewRoom) {
@@ -702,24 +809,26 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
client.store.storeRoom(room);
client.emit("Room", room);
}
stateEvents.forEach(function(e) { client.emit("event", e); });
stateEvents.forEach(function(e) {
client.emit("event", e);
});
});
// Handle joins
joinRooms.forEach(function(joinObj) {
var room = joinObj.room;
var stateEvents = self._mapSyncEventsFormat(joinObj.state, room);
var timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room);
var ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral);
var accountDataEvents = self._mapSyncEventsFormat(joinObj.account_data);
const room = joinObj.room;
const stateEvents = self._mapSyncEventsFormat(joinObj.state, room);
const timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room);
const ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral);
const accountDataEvents = self._mapSyncEventsFormat(joinObj.account_data);
// we do this first so it's correct when any of the events fire
if (joinObj.unread_notifications) {
room.setUnreadNotificationCount(
'total', joinObj.unread_notifications.notification_count
'total', joinObj.unread_notifications.notification_count,
);
room.setUnreadNotificationCount(
'highlight', joinObj.unread_notifications.highlight_count
'highlight', joinObj.unread_notifications.highlight_count,
);
}
@@ -730,9 +839,8 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
// events so that clients can start back-paginating.
room.getLiveTimeline().setPaginationToken(
joinObj.timeline.prev_batch, EventTimeline.BACKWARDS);
}
else if (joinObj.timeline.limited) {
var limited = true;
} else if (joinObj.timeline.limited) {
let limited = true;
// we've got a limited sync, so we *probably* have a gap in the
// timeline, so should reset. But we might have been peeking or
@@ -746,8 +854,8 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
// which we'll try to paginate but not get any new events (which
// will stop us linking the empty timeline into the chain).
//
for (var i = timelineEvents.length - 1; i >= 0; i--) {
var eventId = timelineEvents[i].getId();
for (let i = timelineEvents.length - 1; i >= 0; i--) {
const eventId = timelineEvents[i].getId();
if (room.getTimelineForEvent(eventId)) {
debuglog("Already have event " + eventId + " in limited " +
"sync - not resetting");
@@ -775,7 +883,10 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
// timeline.
room.currentState.paginationToken = syncToken;
self._deregisterStateListeners(room);
room.resetLiveTimeline(joinObj.timeline.prev_batch);
room.resetLiveTimeline(
joinObj.timeline.prev_batch,
self.opts.canResetEntireTimeline(room.roomId),
);
// We have to assume any gap in any timeline is
// reason to stop incrementally tracking notifications and
@@ -801,20 +912,28 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
client.store.storeRoom(room);
client.emit("Room", room);
}
stateEvents.forEach(function(e) { client.emit("event", e); });
timelineEvents.forEach(function(e) { client.emit("event", e); });
ephemeralEvents.forEach(function(e) { client.emit("event", e); });
accountDataEvents.forEach(function(e) { client.emit("event", e); });
stateEvents.forEach(function(e) {
client.emit("event", e);
});
timelineEvents.forEach(function(e) {
client.emit("event", e);
});
ephemeralEvents.forEach(function(e) {
client.emit("event", e);
});
accountDataEvents.forEach(function(e) {
client.emit("event", e);
});
});
// Handle leaves (e.g. kicked rooms)
leaveRooms.forEach(function(leaveObj) {
var room = leaveObj.room;
var stateEvents =
const room = leaveObj.room;
const stateEvents =
self._mapSyncEventsFormat(leaveObj.state, room);
var timelineEvents =
const timelineEvents =
self._mapSyncEventsFormat(leaveObj.timeline, room);
var accountDataEvents =
const accountDataEvents =
self._mapSyncEventsFormat(leaveObj.account_data);
self._processRoomEvents(room, stateEvents, timelineEvents);
@@ -826,9 +945,15 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
client.emit("Room", room);
}
stateEvents.forEach(function(e) { client.emit("event", e); });
timelineEvents.forEach(function(e) { client.emit("event", e); });
accountDataEvents.forEach(function(e) { client.emit("event", e); });
stateEvents.forEach(function(e) {
client.emit("event", e);
});
timelineEvents.forEach(function(e) {
client.emit("event", e);
});
accountDataEvents.forEach(function(e) {
client.emit("event", e);
});
});
// update the notification timeline, if appropriate.
@@ -844,6 +969,13 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
client.getNotifTimelineSet().addLiveEvent(event);
});
}
// Handle device list updates
if (this.opts.crypto && data.device_lists && data.device_lists.changed) {
data.device_lists.changed.forEach((u) => {
this.opts.crypto.userDeviceListChanged(u);
});
}
};
/**
@@ -851,7 +983,7 @@ SyncApi.prototype._processSyncResponse = function(syncToken, data) {
* @param {number} delay How long to delay until the first poll.
* defaults to a short, randomised interval (to prevent
* tightlooping if /versions succeeds but /sync etc. fail).
* @return {promise}
* @return {promise} which resolves once the connection returns
*/
SyncApi.prototype._startKeepAlives = function(delay) {
if (delay === undefined) {
@@ -861,11 +993,11 @@ SyncApi.prototype._startKeepAlives = function(delay) {
if (this._keepAliveTimer !== null) {
clearTimeout(this._keepAliveTimer);
}
var self = this;
const self = this;
if (delay > 0) {
self._keepAliveTimer = setTimeout(
self._pokeKeepAlive.bind(self),
delay
delay,
);
} else {
self._pokeKeepAlive();
@@ -877,10 +1009,14 @@ SyncApi.prototype._startKeepAlives = function(delay) {
};
/**
* Make a dummy call to /_matrix/client/versions, to see if the HS is
* reachable.
*
* On failure, schedules a call back to itself. On success, resolves
* this._connectionReturnedDefer.
*/
SyncApi.prototype._pokeKeepAlive = function() {
var self = this;
const self = this;
function success() {
clearTimeout(self._keepAliveTimer);
if (self._connectionReturnedDefer) {
@@ -897,7 +1033,7 @@ SyncApi.prototype._pokeKeepAlive = function() {
{
prefix: '',
localTimeoutMs: 15 * 1000,
}
},
).done(function() {
success();
}, function(err) {
@@ -911,7 +1047,7 @@ SyncApi.prototype._pokeKeepAlive = function() {
} else {
self._keepAliveTimer = setTimeout(
self._pokeKeepAlive.bind(self),
5000 + Math.floor(Math.random() * 5000)
5000 + Math.floor(Math.random() * 5000),
);
// A keepalive has failed, so we emit the
// error state (whether or not this is the
@@ -932,12 +1068,12 @@ SyncApi.prototype._mapSyncResponseToRoomArray = function(obj) {
// Maps { roomid: {stuff}, roomid: {stuff} }
// to
// [{stuff+Room+isBrandNewRoom}, {stuff+Room+isBrandNewRoom}]
var client = this.client;
var self = this;
const client = this.client;
const self = this;
return utils.keys(obj).map(function(roomId) {
var arrObj = obj[roomId];
var room = client.store.getRoom(roomId);
var isBrandNewRoom = false;
const arrObj = obj[roomId];
let room = client.store.getRoom(roomId);
let isBrandNewRoom = false;
if (!room) {
room = self.createRoom(roomId);
isBrandNewRoom = true;
@@ -957,7 +1093,7 @@ SyncApi.prototype._mapSyncEventsFormat = function(obj, room) {
if (!obj || !utils.isArray(obj.events)) {
return [];
}
var mapper = this.client.getEventMapper();
const mapper = this.client.getEventMapper();
return obj.events.map(function(e) {
if (room) {
e.room_id = room.roomId;
@@ -973,7 +1109,7 @@ SyncApi.prototype._resolveInvites = function(room) {
if (!room || !this.opts.resolveInvitesToProfiles) {
return;
}
var client = this.client;
const client = this.client;
// For each invited room member we want to give them a displayname/avatar url
// if they have one (the m.room.member invites don't contain this).
room.getMembersWithMembership("invite").forEach(function(member) {
@@ -982,22 +1118,21 @@ SyncApi.prototype._resolveInvites = function(room) {
}
member._requestedProfileInfo = true;
// try to get a cached copy first.
var user = client.getUser(member.userId);
var promise;
const user = client.getUser(member.userId);
let promise;
if (user) {
promise = q({
avatar_url: user.avatarUrl,
displayname: user.displayName
displayname: user.displayName,
});
}
else {
} else {
promise = client.getProfileInfo(member.userId);
}
promise.done(function(info) {
// slightly naughty by doctoring the invite event but this means all
// the code paths remain the same between invite/join display name stuff
// which is a worthy trade-off for some minor pollution.
var inviteEvent = member.events.member;
const inviteEvent = member.events.member;
if (inviteEvent.getContent().membership !== "invite") {
// between resolving and now they have since joined, so don't clobber
return;
@@ -1016,23 +1151,25 @@ SyncApi.prototype._resolveInvites = function(room) {
* @param {Room} room
* @param {MatrixEvent[]} stateEventList A list of state events. This is the state
* at the *START* of the timeline list if it is supplied.
* @param {?MatrixEvent[]} timelineEventList A list of timeline events. Lower index
* @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index
* is earlier in time. Higher index is later.
*/
SyncApi.prototype._processRoomEvents = function(room, stateEventList,
timelineEventList) {
timelineEventList = timelineEventList || [];
var client = this.client;
const client = this.client;
// "old" and "current" state are the same initially; they
// start diverging if the user paginates.
// We must deep copy otherwise membership changes in old state
// will leak through to current state!
var oldStateEvents = utils.map(
const oldStateEvents = utils.map(
utils.deepCopy(
stateEventList.map(function(mxEvent) { return mxEvent.event; })
), client.getEventMapper()
stateEventList.map(function(mxEvent) {
return mxEvent.event;
}),
), client.getEventMapper(),
);
var stateEvents = stateEventList;
const stateEvents = stateEventList;
// set the state of the room to as it was before the timeline executes
//
@@ -1051,11 +1188,10 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
// gather our notifications into this._notifEvents
if (client.getNotifTimelineSet()) {
for (var i = 0; i < timelineEventList.length; i++) {
var pushActions = client.getPushActionsForEvent(timelineEventList[i]);
for (let i = 0; i < timelineEventList.length; i++) {
const pushActions = client.getPushActionsForEvent(timelineEventList[i]);
if (pushActions && pushActions.notify &&
pushActions.tweaks && pushActions.tweaks.highlight)
{
pushActions.tweaks && pushActions.tweaks.highlight) {
this._notifEvents.push(timelineEventList[i]);
}
}
@@ -1070,7 +1206,7 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
* @return {string}
*/
SyncApi.prototype._getGuestFilter = function() {
var guestRooms = this.client._guestRooms; // FIXME: horrible gut-wrenching
const guestRooms = this.client._guestRooms; // FIXME: horrible gut-wrenching
if (!guestRooms) {
return "{}";
}
@@ -1079,9 +1215,9 @@ SyncApi.prototype._getGuestFilter = function() {
return JSON.stringify({
room: {
timeline: {
limit: 20
}
}
limit: 20,
},
},
});
};
@@ -1091,7 +1227,7 @@ SyncApi.prototype._getGuestFilter = function() {
* @param {Object} data Object of additional data to emit in the event
*/
SyncApi.prototype._updateSyncState = function(newState, data) {
var old = this._syncState;
const old = this._syncState;
this._syncState = newState;
this.client.emit("sync", this._syncState, old, data);
};
@@ -1108,10 +1244,10 @@ SyncApi.prototype._onOnline = function() {
};
function createNewUser(client, userId) {
var user = new User(userId);
const user = new User(userId);
reEmit(client, user, [
"User.avatarUrl", "User.displayName", "User.presence",
"User.currentlyActive", "User.lastPresenceTs"
"User.currentlyActive", "User.lastPresenceTs",
]);
return user;
}
@@ -1125,11 +1261,11 @@ function reEmit(reEmitEntity, emittableEntity, eventNames) {
// Transformation Example:
// listener on "foo" => function(a,b) { ... }
// Re-emit on "thing" => thing.emit("foo", a, b)
var newArgs = [eventName];
for (var i = 0; i < arguments.length; i++) {
const newArgs = [eventName];
for (let i = 0; i < arguments.length; i++) {
newArgs.push(arguments[i]);
}
reEmitEntity.emit.apply(reEmitEntity, newArgs);
reEmitEntity.emit(...newArgs);
});
});
}
@@ -17,25 +17,25 @@ limitations under the License.
/** @module timeline-window */
var q = require("q");
var EventTimeline = require("./models/event-timeline");
const q = require("q");
const EventTimeline = require("./models/event-timeline");
/**
* @private
*/
var DEBUG = false;
const DEBUG = false;
/**
* @private
*/
var debuglog = DEBUG ? console.log.bind(console) : function() {};
const debuglog = DEBUG ? console.log.bind(console) : function() {};
/**
* the number of times we ask the server for more events before giving up
*
* @private
*/
var DEFAULT_PAGINATE_LOOP_LIMIT = 5;
const DEFAULT_PAGINATE_LOOP_LIMIT = 5;
/**
* Construct a TimelineWindow.
@@ -92,15 +92,15 @@ function TimelineWindow(client, timelineSet, opts) {
* @return {module:client.Promise}
*/
TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
var self = this;
const self = this;
initialWindowSize = initialWindowSize || 20;
// given an EventTimeline, and an event index within it, initialise our
// fields so that the event in question is in the middle of the window.
var initFields = function(timeline, eventIndex) {
var endIndex = Math.min(timeline.getEvents().length,
const initFields = function(timeline, eventIndex) {
const endIndex = Math.min(timeline.getEvents().length,
eventIndex + Math.ceil(initialWindowSize / 2));
var startIndex = Math.max(0, endIndex - initialWindowSize);
const startIndex = Math.max(0, endIndex - initialWindowSize);
self._start = new TimelineIndex(timeline, startIndex - timeline.getBaseIndex());
self._end = new TimelineIndex(timeline, endIndex - timeline.getBaseIndex());
self._eventCount = endIndex - startIndex;
@@ -116,7 +116,7 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
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++) {
for (let i = 0; i < tl.getEvents().length; i++) {
if (tl.getEvents()[i].getId() == initialEventId) {
initFields(tl, i);
return;
@@ -126,7 +126,7 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
});
} else {
// start with the most recent events
var tl = this._timelineSet.getLiveTimeline();
const tl = this._timelineSet.getLiveTimeline();
initFields(tl, tl.getEvents().length);
return q();
}
@@ -146,7 +146,7 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
* @return {boolean} true if we can paginate in the given direction
*/
TimelineWindow.prototype.canPaginate = function(direction) {
var tl;
let tl;
if (direction == EventTimeline.BACKWARDS) {
tl = this._start;
} else if (direction == EventTimeline.FORWARDS) {
@@ -161,9 +161,13 @@ TimelineWindow.prototype.canPaginate = function(direction) {
}
if (direction == EventTimeline.BACKWARDS) {
if (tl.index > tl.minIndex()) { return true; }
if (tl.index > tl.minIndex()) {
return true;
}
} else {
if (tl.index < tl.maxIndex()) { return true; }
if (tl.index < tl.maxIndex()) {
return true;
}
}
return Boolean(tl.timeline.getNeighbouringTimeline(direction) ||
@@ -205,7 +209,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT;
}
var tl;
let tl;
if (direction == EventTimeline.BACKWARDS) {
tl = this._start;
} else if (direction == EventTimeline.FORWARDS) {
@@ -224,7 +228,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
}
// try moving the cap
var count = (direction == EventTimeline.BACKWARDS) ?
const count = (direction == EventTimeline.BACKWARDS) ?
tl.retreat(size) : tl.advance(size);
if (count) {
@@ -232,7 +236,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
debuglog("TimelineWindow: increased cap by " + count +
" (now " + this._eventCount + ")");
// remove some events from the other end, if necessary
var excess = this._eventCount - this._windowLimit;
const excess = this._eventCount - this._windowLimit;
if (excess > 0) {
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
}
@@ -246,18 +250,18 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
}
// try making a pagination request
var token = tl.timeline.getPaginationToken(direction);
const token = tl.timeline.getPaginationToken(direction);
if (!token) {
debuglog("TimelineWindow: no token");
return q(false);
}
debuglog("TimelineWindow: starting request");
var self = this;
const self = this;
var prom = this._client.paginateEventTimeline(tl.timeline, {
const prom = this._client.paginateEventTimeline(tl.timeline, {
backwards: direction == EventTimeline.BACKWARDS,
limit: size
limit: size,
}).finally(function() {
tl.pendingPaginate = null;
}).then(function(r) {
@@ -294,7 +298,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
* of the timeline.
*/
TimelineWindow.prototype.unpaginate = function(delta, startOfTimeline) {
var tl = startOfTimeline ? this._start : this._end;
const tl = startOfTimeline ? this._start : this._end;
// sanity-check the delta
if (delta > this._eventCount || delta < 0) {
@@ -303,7 +307,7 @@ TimelineWindow.prototype.unpaginate = function(delta, startOfTimeline) {
}
while (delta > 0) {
var count = startOfTimeline ? tl.advance(delta) : tl.retreat(delta);
const count = startOfTimeline ? tl.advance(delta) : tl.retreat(delta);
if (count <= 0) {
// sadness. This shouldn't be possible.
throw new Error(
@@ -330,13 +334,13 @@ TimelineWindow.prototype.getEvents = function() {
return [];
}
var result = [];
const result = [];
// iterate through each timeline between this._start and this._end
// (inclusive).
var timeline = this._start.timeline;
let timeline = this._start.timeline;
while (true) {
var events = timeline.getEvents();
const events = timeline.getEvents();
// For the first timeline in the chain, we want to start at
// this._start.index. For the last timeline in the chain, we want to
@@ -346,7 +350,7 @@ TimelineWindow.prototype.getEvents = function() {
// (Note that both this._start.index and this._end.index are relative
// to their respective timelines' BaseIndex).
//
var startIndex = 0, endIndex = events.length;
let startIndex = 0, endIndex = events.length;
if (timeline === this._start.timeline) {
startIndex = this._start.index + timeline.getBaseIndex();
}
@@ -354,7 +358,7 @@ TimelineWindow.prototype.getEvents = function() {
endIndex = this._end.index + timeline.getBaseIndex();
}
for (var i = startIndex; i < endIndex; i++) {
for (let i = startIndex; i < endIndex; i++) {
result.push(events[i]);
}
@@ -415,7 +419,7 @@ TimelineIndex.prototype.advance = function(delta) {
// first try moving the index in the current timeline. See if there is room
// to do so.
var cappedDelta;
let cappedDelta;
if (delta < 0) {
// we want to wind the index backwards.
//
@@ -443,7 +447,7 @@ TimelineIndex.prototype.advance = function(delta) {
// the index is already at the start/end of the current timeline.
//
// next see if there is a neighbouring timeline to switch to.
var neighbour = this.timeline.getNeighbouringTimeline(
const neighbour = this.timeline.getNeighbouringTimeline(
delta < 0 ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS);
if (neighbour) {
this.timeline = neighbour;
+61 -66
View File
@@ -26,9 +26,11 @@ limitations under the License.
* @return {string} The encoded string e.g. foo=bar&baz=taz
*/
module.exports.encodeParams = function(params) {
var qs = "";
for (var key in params) {
if (!params.hasOwnProperty(key)) { continue; }
let qs = "";
for (const key in params) {
if (!params.hasOwnProperty(key)) {
continue;
}
qs += "&" + encodeURIComponent(key) + "=" +
encodeURIComponent(params[key]);
}
@@ -44,10 +46,12 @@ module.exports.encodeParams = function(params) {
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
*/
module.exports.encodeUri = function(pathTemplate, variables) {
for (var key in variables) {
if (!variables.hasOwnProperty(key)) { continue; }
for (const key in variables) {
if (!variables.hasOwnProperty(key)) {
continue;
}
pathTemplate = pathTemplate.replace(
key, encodeURIComponent(variables[key])
key, encodeURIComponent(variables[key]),
);
}
return pathTemplate;
@@ -61,8 +65,8 @@ module.exports.encodeUri = function(pathTemplate, variables) {
* @return {Array} A new array with the results of the function.
*/
module.exports.map = function(array, fn) {
var results = new Array(array.length);
for (var i = 0; i < array.length; i++) {
const results = new Array(array.length);
for (let i = 0; i < array.length; i++) {
results[i] = fn(array[i]);
}
return results;
@@ -77,8 +81,8 @@ module.exports.map = function(array, fn) {
* @return {Array} A new array with the results of the function.
*/
module.exports.filter = function(array, fn) {
var results = [];
for (var i = 0; i < array.length; i++) {
const results = [];
for (let i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
results.push(array[i]);
}
@@ -92,9 +96,11 @@ module.exports.filter = function(array, fn) {
* @return {string[]} The keys of the object.
*/
module.exports.keys = function(obj) {
var keys = [];
for (var key in obj) {
if (!obj.hasOwnProperty(key)) { continue; }
const keys = [];
for (const key in obj) {
if (!obj.hasOwnProperty(key)) {
continue;
}
keys.push(key);
}
return keys;
@@ -106,9 +112,11 @@ module.exports.keys = function(obj) {
* @return {Array<*>} The values of the object.
*/
module.exports.values = function(obj) {
var values = [];
for (var key in obj) {
if (!obj.hasOwnProperty(key)) { continue; }
const values = [];
for (const key in obj) {
if (!obj.hasOwnProperty(key)) {
continue;
}
values.push(obj[key]);
}
return values;
@@ -121,7 +129,7 @@ module.exports.values = function(obj) {
* function signature <code>fn(element, index)</code>.
*/
module.exports.forEach = function(array, fn) {
for (var i = 0; i < array.length; i++) {
for (let i = 0; i < array.length; i++) {
fn(array[i], i);
}
};
@@ -138,15 +146,14 @@ module.exports.forEach = function(array, fn) {
* the given function.
*/
module.exports.findElement = function(array, fn, reverse) {
var i;
let i;
if (reverse) {
for (i = array.length - 1; i >= 0; i--) {
if (fn(array[i], i, array)) {
return array[i];
}
}
}
else {
} else {
for (i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
return array[i];
@@ -166,8 +173,8 @@ module.exports.findElement = function(array, fn, reverse) {
* @return {boolean} True if an element was removed.
*/
module.exports.removeElement = function(array, fn, reverse) {
var i;
var removed;
let i;
let removed;
if (reverse) {
for (i = array.length - 1; i >= 0; i--) {
if (fn(array[i], i, array)) {
@@ -176,8 +183,7 @@ module.exports.removeElement = function(array, fn, reverse) {
return removed;
}
}
}
else {
} else {
for (i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
removed = array[i];
@@ -215,7 +221,7 @@ module.exports.isArray = function(value) {
* @throws If the object is missing keys.
*/
module.exports.checkObjectHasKeys = function(obj, keys) {
for (var i = 0; i < keys.length; i++) {
for (let i = 0; i < keys.length; i++) {
if (!obj.hasOwnProperty(keys[i])) {
throw new Error("Missing required key: " + keys[i]);
}
@@ -229,8 +235,10 @@ module.exports.checkObjectHasKeys = function(obj, keys) {
* @throws If there are extra keys.
*/
module.exports.checkObjectHasNoAdditionalKeys = function(obj, allowedKeys) {
for (var key in obj) {
if (!obj.hasOwnProperty(key)) { continue; }
for (const key in obj) {
if (!obj.hasOwnProperty(key)) {
continue;
}
if (allowedKeys.indexOf(key) === -1) {
throw new Error("Unknown key: " + key);
}
@@ -255,7 +263,7 @@ module.exports.deepCopy = function(obj) {
*
* @return {boolean} true if the two objects are equal
*/
var deepCompare = module.exports.deepCompare = function(x, y) {
const deepCompare = module.exports.deepCompare = function(x, y) {
// Inspired by
// http://stackoverflow.com/questions/1068834/object-comparison-in-javascript#1144249
@@ -301,7 +309,7 @@ var deepCompare = module.exports.deepCompare = function(x, y) {
return false;
}
for (var i = 0; i < x.length; i++) {
for (let i = 0; i < x.length; i++) {
if (!deepCompare(x[i], y[i])) {
return false;
}
@@ -312,7 +320,7 @@ var deepCompare = module.exports.deepCompare = function(x, y) {
/* jshint -W089 */
// check that all of y's direct keys are in x
var p;
let p;
for (p in y) {
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
@@ -347,17 +355,13 @@ var deepCompare = module.exports.deepCompare = function(x, y) {
* @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) {
const target = arguments[0] || {};
for (let i = 1; i < arguments.length; i++) {
const source = arguments[i];
for (const propName in source) { // eslint-disable-line guard-for-in
target[propName] = source[propName];
}
}
/* jshint +W089 */
return target;
};
@@ -371,22 +375,21 @@ module.exports.runPolyfills = function() {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
if (!Array.prototype.filter) {
Array.prototype.filter = function(fun/*, thisArg*/) {
if (this === void 0 || this === null) {
throw new TypeError();
}
var t = Object(this);
var len = t.length >>> 0;
const t = Object(this);
const len = t.length >>> 0;
if (typeof fun !== 'function') {
throw new TypeError();
}
var res = [];
var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
for (var i = 0; i < len; i++) {
const res = [];
const thisArg = arguments.length >= 2 ? arguments[1] : void 0;
for (let i = 0; i < len; i++) {
if (i in t) {
var val = t[i];
const val = t[i];
// NOTE: Technically this should Object.defineProperty at
// the next index, as push can be affected by
@@ -410,10 +413,8 @@ module.exports.runPolyfills = function() {
// Production steps of ECMA-262, Edition 5, 15.4.4.19
// Reference: http://es5.github.io/#x15.4.4.19
if (!Array.prototype.map) {
Array.prototype.map = function(callback, thisArg) {
var T, A, k;
let T, k;
if (this === null || this === undefined) {
throw new TypeError(' this is null or not defined');
@@ -421,12 +422,12 @@ module.exports.runPolyfills = function() {
// 1. Let O be the result of calling ToObject passing the |this|
// value as the argument.
var O = Object(this);
const O = Object(this);
// 2. Let lenValue be the result of calling the Get internal
// method of O with the argument "length".
// 3. Let len be ToUint32(lenValue).
var len = O.length >>> 0;
const len = O.length >>> 0;
// 4. If IsCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
@@ -442,14 +443,13 @@ module.exports.runPolyfills = function() {
// 6. Let A be a new array created as if by the expression new Array(len)
// where Array is the standard built-in constructor with that name and
// len is the value of len.
A = new Array(len);
const A = new Array(len);
// 7. Let k be 0
k = 0;
// 8. Repeat, while k < len
while (k < len) {
var kValue, mappedValue;
// a. Let Pk be ToString(k).
@@ -459,7 +459,6 @@ module.exports.runPolyfills = function() {
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {
// i. Let kValue be the result of calling the Get internal
// method of O with argument Pk.
kValue = O[k];
@@ -504,10 +503,8 @@ module.exports.runPolyfills = function() {
// Production steps of ECMA-262, Edition 5, 15.4.4.18
// Reference: http://es5.github.io/#x15.4.4.18
if (!Array.prototype.forEach) {
Array.prototype.forEach = function(callback, thisArg) {
var T, k;
let T, k;
if (this === null || this === undefined) {
throw new TypeError(' this is null or not defined');
@@ -515,12 +512,12 @@ module.exports.runPolyfills = function() {
// 1. Let O be the result of calling ToObject passing the |this| value as the
// argument.
var O = Object(this);
const O = Object(this);
// 2. Let lenValue be the result of calling the Get internal method of O with the
// argument "length".
// 3. Let len be ToUint32(lenValue).
var len = O.length >>> 0;
const len = O.length >>> 0;
// 4. If IsCallable(callback) is false, throw a TypeError exception.
// See: http://es5.github.com/#x9.11
@@ -538,7 +535,6 @@ module.exports.runPolyfills = function() {
// 7. Repeat, while k < len
while (k < len) {
var kValue;
// a. Let Pk be ToString(k).
@@ -549,7 +545,6 @@ module.exports.runPolyfills = function() {
// This step can be combined with c
// c. If kPresent is true, then
if (k in O) {
// i. Let kValue be the result of calling the Get internal method of O with
// argument Pk
kValue = O[k];
@@ -587,7 +582,7 @@ module.exports.inherits = function(ctor, superCtor) {
function Temp() {}
// make a safe reference to Object.prototype.hasOwnProperty
var hasOwn = Object.prototype.hasOwnProperty;
const hasOwn = Object.prototype.hasOwnProperty;
return function(O) {
// 1. If Type(O) is not Object or Null throw a TypeError exception.
@@ -600,7 +595,7 @@ module.exports.inherits = function(ctor, superCtor) {
// constructor with that name
// 3. Set the [[Prototype]] internal property of obj to O.
Temp.prototype = O;
var obj = new Temp();
const obj = new Temp();
Temp.prototype = null; // Let's not keep a stray reference to O...
// 4. If the argument Properties is present and not undefined, add
@@ -609,8 +604,8 @@ module.exports.inherits = function(ctor, superCtor) {
// Properties.
if (arguments.length > 1) {
// Object.defineProperties does ToObject on its first argument.
var Properties = Object(arguments[1]);
for (var prop in Properties) {
const Properties = Object(arguments[1]);
for (const prop in Properties) {
if (hasOwn.call(Properties, prop)) {
obj[prop] = Properties[prop];
}
@@ -653,7 +648,7 @@ module.exports.inherits = function(ctor, superCtor) {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
configurable: true,
},
});
};
+190 -175
View File
@@ -18,12 +18,53 @@ limitations under the License.
* This is an internal module. See {@link createNewMatrixCall} for the public API.
* @module webrtc/call
*/
var utils = require("../utils");
var EventEmitter = require("events").EventEmitter;
var DEBUG = true; // set true to enable console logging.
const utils = require("../utils");
const EventEmitter = require("events").EventEmitter;
const DEBUG = true; // set true to enable console logging.
// events: hangup, error(err), replaced(call), state(state, oldState)
/**
* Fires when the MatrixCall encounters an error when sending a Matrix event.
* <p>
* This is required to allow errors, which occur during sending of events, to bubble up.
* (This is because call.js does a hangup when it encounters a normal `error`, which in
* turn could lead to an UnknownDeviceError.)
* <p>
* To deal with an UnknownDeviceError when trying to send events, the application should let
* users know that there are new devices in the encrypted room (into which the event was
* sent) and give the user the options to resend unsent events or cancel them. Resending
* is done using {@link module:client~MatrixClient#resendEvent} and cancelling can be done by using
* {@link module:client~MatrixClient#cancelPendingEvent}.
* <p>
* MatrixCall will not do anything in response to an error that causes `send_event_error`
* to be emitted with the exception of sending `m.call.candidates`, which is retried upon
* failure when ICE candidates are being sent. This happens during call setup.
*
* @event module:webrtc/call~MatrixCall#"send_event_error"
* @param {Error} err The error caught from calling client.sendEvent in call.js.
* @example
* matrixCall.on("send_event_error", function(err){
* console.error(err);
* });
*/
/**
* Fires whenever an error occurs when call.js encounters an issue with setting up the call.
* <p>
* The error given will have a code equal to either `MatrixCall.ERR_LOCAL_OFFER_FAILED` or
* `MatrixCall.ERR_NO_USER_MEDIA`. `ERR_LOCAL_OFFER_FAILED` is emitted when the local client
* fails to create an offer. `ERR_NO_USER_MEDIA` is emitted when the user has denied access
* to their audio/video hardware.
*
* @event module:webrtc/call~MatrixCall#"error"
* @param {Error} err The error raised by MatrixCall.
* @example
* matrixCall.on("error", function(err){
* console.error(err.code, err);
* });
*/
/**
* Construct a new Matrix Call.
* @constructor
@@ -43,7 +84,7 @@ function MatrixCall(opts) {
this.turnServers = opts.turnServers || [];
if (this.turnServers.length === 0) {
this.turnServers.push({
urls: [MatrixCall.FALLBACK_STUN_SERVER]
urls: [MatrixCall.FALLBACK_STUN_SERVER],
});
}
utils.forEach(this.turnServers, function(server) {
@@ -122,28 +163,27 @@ MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoEle
* @throws If you have not specified a listener for 'error' events.
*/
MatrixCall.prototype.placeScreenSharingCall =
function(remoteVideoElement, localVideoElement)
{
function(remoteVideoElement, localVideoElement) {
debuglog("placeScreenSharingCall");
checkForErrorListener(this);
var screenConstraints = _getChromeScreenSharingConstraints(this);
const screenConstraints = _getChromeScreenSharingConstraints(this);
if (!screenConstraints) {
return;
}
this.localVideoElement = localVideoElement;
this.remoteVideoElement = remoteVideoElement;
var self = this;
const self = this;
this.webRtc.getUserMedia(screenConstraints, function(stream) {
self.screenSharingStream = stream;
debuglog("Got screen stream, requesting audio stream...");
var audioConstraints = _getUserMediaVideoContraints('voice');
const audioConstraints = _getUserMediaVideoContraints('voice');
_placeCallWithConstraints(self, audioConstraints);
}, function(err) {
self.emit("error",
callError(
MatrixCall.ERR_NO_USER_MEDIA,
"Failed to get screen-sharing stream: " + err
)
"Failed to get screen-sharing stream: " + err,
),
);
});
this.type = 'video';
@@ -153,7 +193,7 @@ MatrixCall.prototype.placeScreenSharingCall =
/**
* 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 {Element} 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) {
@@ -174,8 +214,7 @@ MatrixCall.prototype.playElement = function(element, queueId) {
console.log("previous promise failed for " + queueId);
return element.play();
});
}
else {
} else {
this.mediaPromises[queueId] = element.play();
}
};
@@ -183,7 +222,7 @@ MatrixCall.prototype.playElement = function(element, queueId) {
/**
* 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 {Element} 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) {
@@ -197,8 +236,7 @@ MatrixCall.prototype.pauseElement = function(element, queueId) {
console.log("previous promise failed for " + queueId);
return element.pause();
});
}
else {
} 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();
@@ -209,24 +247,24 @@ MatrixCall.prototype.pauseElement = function(element, queueId) {
* 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 {Element} element HTMLMediaElement element to pause
* @param {MediaStream} srcObject the srcObject 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);
MatrixCall.prototype.assignElement = function(element, srcObject, queueId) {
console.log("queuing assign on " + queueId + " element " + element + " for " +
srcObject);
if (this.mediaPromises[queueId]) {
this.mediaPromises[queueId] =
this.mediaPromises[queueId].then(function() {
console.log("previous promise completed for " + queueId);
element.src = src;
element.srcObject = srcObject;
}, function() {
console.log("previous promise failed for " + queueId);
element.src = src;
element.srcObject = srcObject;
});
}
else {
element.src = src;
} else {
element.srcObject = srcObject;
}
};
@@ -266,13 +304,11 @@ MatrixCall.prototype.setLocalVideoElement = function(element) {
if (element && this.localAVStream && this.type === 'video') {
element.autoplay = true;
this.assignElement(element,
this.URL.createObjectURL(this.localAVStream),
"localVideo");
this.assignElement(element, this.localAVStream, "localVideo");
element.muted = true;
var self = this;
const self = this;
setTimeout(function() {
var vel = self.getLocalVideoElement();
const vel = self.getLocalVideoElement();
if (vel.play) {
self.playElement(vel, "localVideo");
}
@@ -310,12 +346,12 @@ MatrixCall.prototype.setRemoteAudioElement = function(element) {
MatrixCall.prototype._initWithInvite = function(event) {
this.msg = event.getContent();
this.peerConn = _createPeerConnection(this);
var self = this;
const self = this;
if (this.peerConn) {
this.peerConn.setRemoteDescription(
new this.webRtc.RtcSessionDescription(this.msg.offer),
hookCallback(self, self._onSetRemoteDescriptionSuccess),
hookCallback(self, self._onSetRemoteDescriptionError)
hookCallback(self, self._onSetRemoteDescriptionError),
);
}
setState(this, 'ringing');
@@ -330,8 +366,7 @@ MatrixCall.prototype._initWithInvite = function(event) {
this.msg.offer.sdp.indexOf('m=video') > -1
) {
this.type = 'video';
}
else {
} else {
this.type = 'voice';
}
@@ -369,13 +404,13 @@ MatrixCall.prototype._initWithHangup = function(event) {
*/
MatrixCall.prototype.answer = function() {
debuglog("Answering call %s of type %s", this.callId, this.type);
var self = this;
const self = this;
if (!this.localAVStream && !this.waitForLocalAVStream) {
this.webRtc.getUserMedia(
_getUserMediaVideoContraints(this.type),
hookCallback(self, self._gotUserMediaForAnswer),
hookCallback(self, self._getUserMediaFailed)
hookCallback(self, self._getUserMediaFailed),
);
setState(this, 'wait_local_media');
} else if (this.localAVStream) {
@@ -421,10 +456,10 @@ MatrixCall.prototype._replacedBy = function(newCall) {
MatrixCall.prototype.hangup = function(reason, suppressEvent) {
debuglog("Ending call " + this.callId);
terminate(this, "local", reason, !suppressEvent);
var content = {
const content = {
version: 0,
call_id: this.callId,
reason: reason
reason: reason,
};
sendEvent(this, 'm.call.hangup', content);
};
@@ -497,45 +532,39 @@ MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
return;
}
debuglog("_gotUserMediaForInvite -> " + this.type);
var self = this;
var videoEl = this.getLocalVideoElement();
const self = this;
const videoEl = this.getLocalVideoElement();
if (videoEl && this.type == 'video') {
videoEl.autoplay = true;
if (this.screenSharingStream) {
debuglog("Setting screen sharing stream to the local video element");
this.assignElement(videoEl,
this.URL.createObjectURL(this.screenSharingStream),
"localVideo");
}
else {
this.assignElement(videoEl,
this.URL.createObjectURL(stream),
"localVideo");
this.assignElement(videoEl, this.screenSharingStream, "localVideo");
} else {
this.assignElement(videoEl, stream, "localVideo");
}
videoEl.muted = true;
setTimeout(function() {
var vel = self.getLocalVideoElement();
const vel = self.getLocalVideoElement();
if (vel.play) {
self.playElement(vel, "localVideo");
}
}, 0);
}
if (this.screenSharingStream) {
this.screenSharingStream.addTrack(stream.getAudioTracks()[0]);
stream = this.screenSharingStream;
}
this.localAVStream = stream;
// why do we enable audio (and only audio) tracks here? -- matthew
setTracksEnabled(stream.getAudioTracks(), true);
this.peerConn = _createPeerConnection(this);
this.peerConn.addStream(stream);
if (this.screenSharingStream) {
console.log("Adding screen-sharing stream to peer connection");
this.peerConn.addStream(this.screenSharingStream);
// let's use this for the local preview...
this.localAVStream = this.screenSharingStream;
}
this.peerConn.createOffer(
hookCallback(self, self._gotLocalOffer),
hookCallback(self, self._getLocalOfferFailed)
hookCallback(self, self._getLocalOfferFailed),
);
setState(self, 'create_offer');
};
@@ -546,20 +575,18 @@ MatrixCall.prototype._gotUserMediaForInvite = function(stream) {
* @param {Object} stream
*/
MatrixCall.prototype._gotUserMediaForAnswer = function(stream) {
var self = this;
const self = this;
if (self.state == 'ended') {
return;
}
var localVidEl = self.getLocalVideoElement();
const localVidEl = self.getLocalVideoElement();
if (localVidEl && self.type == 'video') {
localVidEl.autoplay = true;
this.assignElement(localVidEl,
this.URL.createObjectURL(stream),
"localVideo");
this.assignElement(localVidEl, stream, "localVideo");
localVidEl.muted = true;
setTimeout(function() {
var vel = self.getLocalVideoElement();
const vel = self.getLocalVideoElement();
if (vel.play) {
self.playElement(vel, "localVideo");
}
@@ -570,22 +597,22 @@ MatrixCall.prototype._gotUserMediaForAnswer = function(stream) {
setTracksEnabled(stream.getAudioTracks(), true);
self.peerConn.addStream(stream);
var constraints = {
const constraints = {
'mandatory': {
'OfferToReceiveAudio': true,
'OfferToReceiveVideo': self.type == 'video'
}
'OfferToReceiveVideo': self.type == 'video',
},
};
self.peerConn.createAnswer(function(description) {
debuglog("Created answer: " + description);
self.peerConn.setLocalDescription(description, function() {
var content = {
const content = {
version: 0,
call_id: self.callId,
answer: {
sdp: self.peerConn.localDescription.sdp,
type: self.peerConn.localDescription.type
}
type: self.peerConn.localDescription.type,
},
};
sendEvent(self, 'm.call.answer', content);
setState(self, 'connecting');
@@ -607,14 +634,14 @@ MatrixCall.prototype._gotLocalIceCandidate = function(event) {
if (event.candidate) {
debuglog(
"Got local ICE " + event.candidate.sdpMid + " candidate: " +
event.candidate.candidate
event.candidate.candidate,
);
// As with the offer, note we need to make a copy of this object, not
// pass the original: that broke in Chrome ~m43.
var c = {
const c = {
candidate: event.candidate.candidate,
sdpMid: event.candidate.sdpMid,
sdpMLineIndex: event.candidate.sdpMLineIndex
sdpMLineIndex: event.candidate.sdpMLineIndex,
};
sendCandidate(this, c);
}
@@ -634,7 +661,7 @@ MatrixCall.prototype._gotRemoteIceCandidate = function(cand) {
this.peerConn.addIceCandidate(
new this.webRtc.RtcIceCandidate(cand),
function() {},
function(e) {}
function(e) {},
);
};
@@ -648,11 +675,11 @@ MatrixCall.prototype._receivedAnswer = function(msg) {
return;
}
var self = this;
const self = this;
this.peerConn.setRemoteDescription(
new this.webRtc.RtcSessionDescription(msg.answer),
hookCallback(self, self._onSetRemoteDescriptionSuccess),
hookCallback(self, self._onSetRemoteDescriptionError)
hookCallback(self, self._onSetRemoteDescriptionError),
);
setState(self, 'connecting');
};
@@ -663,7 +690,7 @@ MatrixCall.prototype._receivedAnswer = function(msg) {
* @param {Object} description
*/
MatrixCall.prototype._gotLocalOffer = function(description) {
var self = this;
const self = this;
debuglog("Created offer: " + description);
if (self.state == 'ended') {
@@ -673,7 +700,7 @@ MatrixCall.prototype._gotLocalOffer = function(description) {
}
self.peerConn.setLocalDescription(description, function() {
var content = {
const content = {
version: 0,
call_id: self.callId,
// OpenWebRTC appears to add extra stuff (like the DTLS fingerprint)
@@ -687,9 +714,9 @@ MatrixCall.prototype._gotLocalOffer = function(description) {
// Chrome (as of about m43).
offer: {
sdp: self.peerConn.localDescription.sdp,
type: self.peerConn.localDescription.type
type: self.peerConn.localDescription.type,
},
lifetime: MatrixCall.CALL_TIMEOUT_MS
lifetime: MatrixCall.CALL_TIMEOUT_MS,
};
sendEvent(self, 'm.call.invite', content);
@@ -712,7 +739,7 @@ MatrixCall.prototype._gotLocalOffer = function(description) {
MatrixCall.prototype._getLocalOfferFailed = function(error) {
this.emit(
"error",
callError(MatrixCall.ERR_LOCAL_OFFER_FAILED, "Failed to start audio for call!")
callError(MatrixCall.ERR_LOCAL_OFFER_FAILED, "Failed to start audio for call!"),
);
};
@@ -727,8 +754,8 @@ MatrixCall.prototype._getUserMediaFailed = function(error) {
callError(
MatrixCall.ERR_NO_USER_MEDIA,
"Couldn't start capturing media! Is your microphone set up and " +
"does this app have permission?"
)
"does this app have permission?",
),
);
this.hangup("user_media_failed");
};
@@ -742,7 +769,7 @@ MatrixCall.prototype._onIceConnectionStateChanged = function() {
return; // because ICE can still complete as we're ending the call
}
debuglog(
"Ice connection state changed to: " + this.peerConn.iceConnectionState
"Ice connection state changed to: " + this.peerConn.iceConnectionState,
);
// ideally we'd consider the call to be connected when we get media but
// chrome doesn't implement any of the 'onstarted' events yet
@@ -762,7 +789,7 @@ MatrixCall.prototype._onIceConnectionStateChanged = function() {
MatrixCall.prototype._onSignallingStateChanged = function() {
debuglog(
"call " + this.callId + ": Signalling state changed to: " +
this.peerConn.signalingState
this.peerConn.signalingState,
);
};
@@ -791,7 +818,7 @@ MatrixCall.prototype._onSetRemoteDescriptionError = function(e) {
MatrixCall.prototype._onAddStream = function(event) {
debuglog("Stream id " + event.stream.id + " added");
var s = event.stream;
const s = event.stream;
if (s.getVideoTracks().length > 0) {
this.type = 'video';
@@ -802,7 +829,7 @@ MatrixCall.prototype._onAddStream = function(event) {
this.remoteAStream = s;
}
var self = this;
const self = this;
forAllTracksOnStream(s, function(t) {
debuglog("Track id " + t.id + " added");
// not currently implemented in chrome
@@ -811,8 +838,7 @@ MatrixCall.prototype._onAddStream = function(event) {
if (event.stream.oninactive !== undefined) {
event.stream.oninactive = hookCallback(self, self._onRemoteStreamEnded);
}
else {
} else {
// onended is deprecated from Chrome 54
event.stream.onended = hookCallback(self, self._onRemoteStreamEnded);
}
@@ -823,8 +849,7 @@ MatrixCall.prototype._onAddStream = function(event) {
if (this.type === 'video') {
_tryPlayRemoteStream(this);
_tryPlayRemoteAudioStream(this);
}
else {
} else {
_tryPlayRemoteAudioStream(this);
}
};
@@ -883,14 +908,14 @@ MatrixCall.prototype._onAnsweredElsewhere = function(msg) {
terminate(this, "remote", "answered_elsewhere", true);
};
var setTracksEnabled = function(tracks, enabled) {
for (var i = 0; i < tracks.length; i++) {
const setTracksEnabled = function(tracks, enabled) {
for (let i = 0; i < tracks.length; i++) {
tracks[i].enabled = enabled;
}
};
var isTracksEnabled = function(tracks) {
for (var i = 0; i < tracks.length; i++) {
const isTracksEnabled = function(tracks) {
for (let i = 0; i < tracks.length; i++) {
if (tracks[i].enabled) {
return true; // at least one track is enabled
}
@@ -898,8 +923,8 @@ var isTracksEnabled = function(tracks) {
return false;
};
var setState = function(self, state) {
var oldState = self.state;
const setState = function(self, state) {
const oldState = self.state;
self.state = state;
self.emit("state", state, oldState);
};
@@ -911,11 +936,15 @@ var setState = function(self, state) {
* @param {Object} content
* @return {Promise}
*/
var sendEvent = function(self, eventType, content) {
return self.client.sendEvent(self.roomId, eventType, content);
const sendEvent = function(self, eventType, content) {
return self.client.sendEvent(self.roomId, eventType, content).catch(
(err) => {
self.emit('send_event_error', err);
},
);
};
var sendCandidate = function(self, content) {
const sendCandidate = function(self, content) {
// Sends candidates with are sent in a special way because we try to amalgamate
// them into one message
self.candidateSendQueue.push(content);
@@ -926,24 +955,24 @@ var sendCandidate = function(self, content) {
}
};
var terminate = function(self, hangupParty, hangupReason, shouldEmit) {
const terminate = function(self, hangupParty, hangupReason, shouldEmit) {
if (self.getRemoteVideoElement()) {
if (self.getRemoteVideoElement().pause) {
self.pauseElement(self.getRemoteVideoElement(), "remoteVideo");
}
self.assignElement(self.getRemoteVideoElement(), "", "remoteVideo");
self.assignElement(self.getRemoteVideoElement(), null, "remoteVideo");
}
if (self.getRemoteAudioElement()) {
if (self.getRemoteAudioElement().pause) {
self.pauseElement(self.getRemoteAudioElement(), "remoteAudio");
}
self.assignElement(self.getRemoteAudioElement(), "", "remoteAudio");
self.assignElement(self.getRemoteAudioElement(), null, "remoteAudio");
}
if (self.getLocalVideoElement()) {
if (self.getLocalVideoElement().pause) {
self.pauseElement(self.getLocalVideoElement(), "localVideo");
}
self.assignElement(self.getLocalVideoElement(), "", "localVideo");
self.assignElement(self.getLocalVideoElement(), null, "localVideo");
}
self.hangupParty = hangupParty;
self.hangupReason = hangupReason;
@@ -957,7 +986,7 @@ var terminate = function(self, hangupParty, hangupReason, shouldEmit) {
}
};
var stopAllMedia = function(self) {
const stopAllMedia = function(self) {
debuglog("stopAllMedia (stream=%s)", self.localAVStream);
if (self.localAVStream) {
forAllTracksOnStream(self.localAVStream, function(t) {
@@ -997,15 +1026,13 @@ var stopAllMedia = function(self) {
}
};
var _tryPlayRemoteStream = function(self) {
const _tryPlayRemoteStream = function(self) {
if (self.getRemoteVideoElement() && self.remoteAVStream) {
var player = self.getRemoteVideoElement();
const player = self.getRemoteVideoElement();
player.autoplay = true;
self.assignElement(player,
self.URL.createObjectURL(self.remoteAVStream),
"remoteVideo");
self.assignElement(player, self.remoteAVStream, "remoteVideo");
setTimeout(function() {
var vel = self.getRemoteVideoElement();
const vel = self.getRemoteVideoElement();
if (vel.play) {
self.playElement(vel, "remoteVideo");
}
@@ -1017,15 +1044,13 @@ var _tryPlayRemoteStream = function(self) {
}
};
var _tryPlayRemoteAudioStream = function(self) {
const _tryPlayRemoteAudioStream = function(self) {
if (self.getRemoteAudioElement() && self.remoteAStream) {
var player = self.getRemoteAudioElement();
const player = self.getRemoteAudioElement();
player.autoplay = true;
self.assignElement(player,
self.URL.createObjectURL(self.remoteAStream),
"remoteAudio");
self.assignElement(player, self.remoteAStream, "remoteAudio");
setTimeout(function() {
var ael = self.getRemoteAudioElement();
const ael = self.getRemoteAudioElement();
if (ael.play) {
self.playElement(ael, "remoteAudio");
}
@@ -1037,58 +1062,58 @@ var _tryPlayRemoteAudioStream = function(self) {
}
};
var checkForErrorListener = function(self) {
const checkForErrorListener = function(self) {
if (self.listeners("error").length === 0) {
throw new Error(
"You MUST attach an error listener using call.on('error', function() {})"
"You MUST attach an error listener using call.on('error', function() {})",
);
}
};
var callError = function(code, msg) {
var e = new Error(msg);
const callError = function(code, msg) {
const e = new Error(msg);
e.code = code;
return e;
};
var debuglog = function() {
const debuglog = function() {
if (DEBUG) {
console.log.apply(console, arguments);
console.log(...arguments);
}
};
var _sendCandidateQueue = function(self) {
const _sendCandidateQueue = function(self) {
if (self.candidateSendQueue.length === 0) {
return;
}
var cands = self.candidateSendQueue;
const cands = self.candidateSendQueue;
self.candidateSendQueue = [];
++self.candidateSendTries;
var content = {
const content = {
version: 0,
call_id: self.callId,
candidates: cands
candidates: cands,
};
debuglog("Attempting to send " + cands.length + " candidates");
sendEvent(self, 'm.call.candidates', content).then(function() {
self.candidateSendTries = 0;
_sendCandidateQueue(self);
}, function(error) {
for (var i = 0; i < cands.length; i++) {
for (let i = 0; i < cands.length; i++) {
self.candidateSendQueue.push(cands[i]);
}
if (self.candidateSendTries > 5) {
debuglog(
"Failed to send candidates on attempt %s. Giving up for now.",
self.candidateSendTries
self.candidateSendTries,
);
self.candidateSendTries = 0;
return;
}
var delayMs = 500 * Math.pow(2, self.candidateSendTries);
const delayMs = 500 * Math.pow(2, self.candidateSendTries);
++self.candidateSendTries;
debuglog("Failed to send candidates. Retrying in " + delayMs + "ms");
setTimeout(function() {
@@ -1097,36 +1122,36 @@ var _sendCandidateQueue = function(self) {
});
};
var _placeCallWithConstraints = function(self, constraints) {
const _placeCallWithConstraints = function(self, constraints) {
self.client.callList[self.callId] = self;
self.webRtc.getUserMedia(
constraints,
hookCallback(self, self._gotUserMediaForInvite),
hookCallback(self, self._getUserMediaFailed)
hookCallback(self, self._getUserMediaFailed),
);
setState(self, 'wait_local_media');
self.direction = 'outbound';
self.config = constraints;
};
var _createPeerConnection = function(self) {
var servers = self.turnServers;
const _createPeerConnection = function(self) {
let servers = self.turnServers;
if (self.webRtc.vendor === "mozilla") {
// modify turnServers struct to match what mozilla expects.
servers = [];
for (var i = 0; i < self.turnServers.length; i++) {
for (var j = 0; j < self.turnServers[i].urls.length; j++) {
for (let i = 0; i < self.turnServers.length; i++) {
for (let j = 0; j < self.turnServers[i].urls.length; j++) {
servers.push({
url: self.turnServers[i].urls[j],
username: self.turnServers[i].username,
credential: self.turnServers[i].credential
credential: self.turnServers[i].credential,
});
}
}
}
var pc = new self.webRtc.RtcPeerConnection({
iceServers: servers
const pc = new self.webRtc.RtcPeerConnection({
iceServers: servers,
});
pc.oniceconnectionstatechange = hookCallback(self, self._onIceConnectionStateChanged);
pc.onsignalingstatechange = hookCallback(self, self._onSignallingStateChanged);
@@ -1135,20 +1160,12 @@ var _createPeerConnection = function(self) {
return pc;
};
var _getChromeScreenSharingConstraints = function(call) {
var screen = global.screen;
const _getChromeScreenSharingConstraints = function(call) {
const screen = global.screen;
if (!screen) {
call.emit("error", callError(
MatrixCall.ERR_NO_USER_MEDIA,
"Couldn't determine screen sharing constaints."
));
return;
}
// it won't work at all if you're not on HTTPS so whine whine whine
if (!global.window || global.window.location.protocol !== "https:") {
call.emit("error", callError(
MatrixCall.ERR_NO_USER_MEDIA,
"You need to be using HTTPS to place a screen-sharing call."
"Couldn't determine screen sharing constaints.",
));
return;
}
@@ -1161,13 +1178,13 @@ var _getChromeScreenSharingConstraints = function(call) {
maxWidth: screen.width,
maxHeight: screen.height,
minFrameRate: 1,
maxFrameRate: 10
}
}
maxFrameRate: 10,
},
},
};
};
var _getUserMediaVideoContraints = function(callType) {
const _getUserMediaVideoContraints = function(callType) {
switch (callType) {
case 'voice':
return ({audio: true, video: false});
@@ -1177,33 +1194,33 @@ var _getUserMediaVideoContraints = function(callType) {
minWidth: 640,
maxWidth: 640,
minHeight: 360,
maxHeight: 360
}
maxHeight: 360,
},
}});
}
};
var hookCallback = function(call, fn) {
const hookCallback = function(call, fn) {
return function() {
return fn.apply(call, arguments);
};
};
var forAllVideoTracksOnStream = function(s, f) {
var tracks = s.getVideoTracks();
for (var i = 0; i < tracks.length; i++) {
const forAllVideoTracksOnStream = function(s, f) {
const tracks = s.getVideoTracks();
for (let i = 0; i < tracks.length; i++) {
f(tracks[i]);
}
};
var forAllAudioTracksOnStream = function(s, f) {
var tracks = s.getAudioTracks();
for (var i = 0; i < tracks.length; i++) {
const forAllAudioTracksOnStream = function(s, f) {
const tracks = s.getAudioTracks();
for (let i = 0; i < tracks.length; i++) {
f(tracks[i]);
}
};
var forAllTracksOnStream = function(s, f) {
const forAllTracksOnStream = function(s, f) {
forAllVideoTracksOnStream(s, f);
forAllAudioTracksOnStream(s, f);
};
@@ -1219,25 +1236,25 @@ module.exports.MatrixCall = MatrixCall;
* @return {MatrixCall} the call or null if the browser doesn't support calling.
*/
module.exports.createNewMatrixCall = function(client, roomId) {
var w = global.window;
var doc = global.document;
const w = global.window;
const doc = global.document;
if (!w || !doc) {
return null;
}
var webRtc = {};
const webRtc = {};
webRtc.isOpenWebRTC = function() {
var scripts = doc.getElementById("script");
const scripts = doc.getElementById("script");
if (!scripts || !scripts.length) {
return false;
}
for (var i = 0; i < scripts.length; i++) {
for (let i = 0; i < scripts.length; i++) {
if (scripts[i].src.indexOf("owr.js") > -1) {
return true;
}
}
return false;
};
var getUserMedia = (
const getUserMedia = (
w.navigator.getUserMedia || w.navigator.webkitGetUserMedia ||
w.navigator.mozGetUserMedia
);
@@ -1259,23 +1276,21 @@ module.exports.createNewMatrixCall = function(client, roomId) {
webRtc.vendor = null;
if (w.mozRTCPeerConnection) {
webRtc.vendor = "mozilla";
}
else if (w.webkitRTCPeerConnection) {
} else if (w.webkitRTCPeerConnection) {
webRtc.vendor = "webkit";
}
else if (w.RTCPeerConnection) {
} else if (w.RTCPeerConnection) {
webRtc.vendor = "generic";
}
if (!webRtc.RtcIceCandidate || !webRtc.RtcSessionDescription ||
!webRtc.RtcPeerConnection || !webRtc.getUserMedia) {
return null; // WebRTC is not supported.
}
var opts = {
const opts = {
webRtc: webRtc,
client: client,
URL: w.URL,
roomId: roomId,
turnServers: client.getTurnServers()
turnServers: client.getTurnServers(),
};
return new MatrixCall(opts);
};