Compare commits

...

299 Commits

Author SHA1 Message Date
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
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 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
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
Matthew Hodgson 5d544c773d Merge branch 'develop' into matthew/warn-unknown-devices 2017-02-01 22:35:25 +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
Richard van der Hoff b7e33b237b Update browserify
... to a more recent release, for compatibility with sourceify.
2017-02-01 00:50:22 +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
Richard van der Hoff e3b008e19a Make browserify a dev dependency 2017-01-26 16:40:19 +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
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
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
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 79a699f0be v0.7.1 2016-12-09 19:29:13 +00:00
David Baker 4a2e6a826b Prepare changelog for v0.7.1 2016-12-09 19:29:12 +00:00
David Baker 95f56f95ec Merge pull request #305 from matrix-org/dbkr/signed_releases
Update release script to do signed releases
2016-12-09 19:22:01 +00:00
David Baker 67e75fb7af Invoke changelog_head from outside the pushd
so we know the relative path is right
2016-12-09 19:19:40 +00:00
David Baker ebda89d1ae Add changelog_head.py 2016-12-09 17:34:11 +00:00
David Baker 5ce5299651 Update release script to do signed releases
if a signing ID is set in release_config.yaml

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

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

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

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

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

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

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

Also:

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

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

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

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

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

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

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

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

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

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

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

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

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

Also:
 * Use request rather than deprecated requestWithPrefix
 * http-api: The empty string may be falsy, but it's a valid prefix
2016-10-06 18:29:05 +01:00
David Baker 57cf7e1f7d Merge pull request #222 from matrix-org/revert-216-erikj/sync_fail_first
Revert "Handle the first /sync failure differently."
2016-10-06 16:28:45 +01:00
David Baker 86ea00cfee Revert "Handle the first /sync failure differently." 2016-10-06 16:27:38 +01:00
121 changed files with 8380 additions and 344168 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"presets": ["es2015"]
}
+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"],
}
}
+3 -3
View File
@@ -1,5 +1,3 @@
# Keep this file in sync with .npmignore.
.jsdoc
node_modules
.lock-wscript
@@ -8,7 +6,9 @@ coverage
lib-cov
out
reports
dist/browser-matrix-dev.js
/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"
}
-15
View File
@@ -1,15 +0,0 @@
# Keep this file in sync with .gitignore.
.jsdoc
node_modules
.lock-wscript
build/Release
coverage
lib-cov
out
reports
dist/browser-matrix-dev.js
# tarball created by 'npm pack'.
/matrix-js-sdk-*.tgz
+6
View File
@@ -0,0 +1,6 @@
language: node_js
node_js:
- node # Latest stable version of nodejs.
script:
- npm run lint
- npm run test
+225
View File
@@ -1,3 +1,228 @@
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)
No changes
Changes in [0.7.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.1-rc.1) (2016-12-05)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.0...v0.7.1-rc.1)
* Avoid NPE when no sessionStore is given
[\#300](https://github.com/matrix-org/matrix-js-sdk/pull/300)
* Improve decryption error messages
[\#299](https://github.com/matrix-org/matrix-js-sdk/pull/299)
* Revert "Use native Array.isArray when available."
[\#283](https://github.com/matrix-org/matrix-js-sdk/pull/283)
* Use native Array.isArray when available.
[\#282](https://github.com/matrix-org/matrix-js-sdk/pull/282)
Changes in [0.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.0) (2016-11-18)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4...v0.7.0)
* Avoid a packetstorm of device queries on startup
[\#297](https://github.com/matrix-org/matrix-js-sdk/pull/297)
* E2E: Check devices to share keys with on each send
[\#295](https://github.com/matrix-org/matrix-js-sdk/pull/295)
* Apply unknown-keyshare mitigations
[\#296](https://github.com/matrix-org/matrix-js-sdk/pull/296)
* distinguish unknown users from deviceless users
[\#294](https://github.com/matrix-org/matrix-js-sdk/pull/294)
* Allow starting client with initialSyncLimit = 0
[\#293](https://github.com/matrix-org/matrix-js-sdk/pull/293)
* Make timeline-window _unpaginate public and rename to unpaginate
[\#289](https://github.com/matrix-org/matrix-js-sdk/pull/289)
* Send a STOPPED sync updated after call to stopClient
[\#286](https://github.com/matrix-org/matrix-js-sdk/pull/286)
* Fix bug in verifying megolm event senders
[\#292](https://github.com/matrix-org/matrix-js-sdk/pull/292)
* Handle decryption of events after they arrive
[\#288](https://github.com/matrix-org/matrix-js-sdk/pull/288)
* Fix examples.
[\#287](https://github.com/matrix-org/matrix-js-sdk/pull/287)
* Add a travis.yml
[\#278](https://github.com/matrix-org/matrix-js-sdk/pull/278)
* Encrypt all events, including 'm.call.*'
[\#277](https://github.com/matrix-org/matrix-js-sdk/pull/277)
* Ignore reshares of known megolm sessions
[\#276](https://github.com/matrix-org/matrix-js-sdk/pull/276)
* Log to the console on unknown session
[\#274](https://github.com/matrix-org/matrix-js-sdk/pull/274)
* Make it easier for SDK users to wrap prevailing the 'request' function
[\#273](https://github.com/matrix-org/matrix-js-sdk/pull/273)
Changes in [0.6.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4) (2016-11-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4-rc.2...v0.6.4)
* Change release script to pass version by environment variable
Changes in [0.6.4-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4-rc.2) (2016-11-02)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4-rc.1...v0.6.4-rc.2)
* Add getRoomTags method to client
[\#236](https://github.com/matrix-org/matrix-js-sdk/pull/236)
Changes in [0.6.4-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4-rc.1) (2016-11-02)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.3...v0.6.4-rc.1)
Breaking Changes
----------------
* Bundled version of the JS SDK are no longer versioned along with
source files in the dist/ directory. As of this release, they
will be included in the release tarball, but not the source
repository.
Other Changes
-------------
* More fixes to the release script
[\#272](https://github.com/matrix-org/matrix-js-sdk/pull/272)
* Update the release process to use github releases
[\#271](https://github.com/matrix-org/matrix-js-sdk/pull/271)
* Don't package the world when we release
[\#270](https://github.com/matrix-org/matrix-js-sdk/pull/270)
* Add ability to set a filter prior to the first /sync
[\#269](https://github.com/matrix-org/matrix-js-sdk/pull/269)
* Sign one-time keys, and verify their signatures
[\#243](https://github.com/matrix-org/matrix-js-sdk/pull/243)
* Check for duplicate message indexes for group messages
[\#241](https://github.com/matrix-org/matrix-js-sdk/pull/241)
* Rotate megolm sessions
[\#240](https://github.com/matrix-org/matrix-js-sdk/pull/240)
* Check recipient and sender in Olm messages
[\#239](https://github.com/matrix-org/matrix-js-sdk/pull/239)
* Consistency checks for E2E device downloads
[\#237](https://github.com/matrix-org/matrix-js-sdk/pull/237)
* Support User-Interactive auth for delete device
[\#235](https://github.com/matrix-org/matrix-js-sdk/pull/235)
* Utility to help with interactive auth
[\#234](https://github.com/matrix-org/matrix-js-sdk/pull/234)
* Fix sync breaking when an invalid filterId is in localStorage
[\#228](https://github.com/matrix-org/matrix-js-sdk/pull/228)
Changes in [0.6.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.3) (2016-10-12)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.2...v0.6.3)
Breaking Changes
----------------
* Add a 'RECONNECTING' state to the sync states. This is an additional state
between 'SYNCING' and 'ERROR', so most clients should not notice.
Other Changes
----------------
* Fix params getting replaced on register calls
[\#233](https://github.com/matrix-org/matrix-js-sdk/pull/233)
* Fix potential 30s delay on reconnect
[\#232](https://github.com/matrix-org/matrix-js-sdk/pull/232)
* uploadContent: Attempt some consistency between browser and node
[\#230](https://github.com/matrix-org/matrix-js-sdk/pull/230)
* Fix error handling on uploadContent
[\#229](https://github.com/matrix-org/matrix-js-sdk/pull/229)
* Fix uploadContent for node.js
[\#226](https://github.com/matrix-org/matrix-js-sdk/pull/226)
* Don't emit ERROR until a keepalive poke fails
[\#223](https://github.com/matrix-org/matrix-js-sdk/pull/223)
* Function to get the fallback url for interactive auth
[\#224](https://github.com/matrix-org/matrix-js-sdk/pull/224)
* Revert "Handle the first /sync failure differently."
[\#222](https://github.com/matrix-org/matrix-js-sdk/pull/222)
Changes in [0.6.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.2) (2016-10-05)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.1...v0.6.2)
+4
View File
@@ -0,0 +1,4 @@
Contributing code to matrix-js-sdk
==================================
matrix-js-sdk follows the same pattern as https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst
+5 -3
View File
@@ -10,9 +10,10 @@ Quickstart
In a browser
------------
Copy ``dist/$VERSION/browser-matrix-$VERSION.js`` and add that as a ``<script>`` to
your page. There will be a global variable ``matrixcs`` attached to
``window`` through which you can access the SDK.
Download either the full or minified version from
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
``<script>`` to your page. There will be a global variable ``matrixcs``
attached to ``window`` through which you can access the SDK.
Please check [the working browser example](examples/browser) for more information.
@@ -64,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`.
-5826
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-6490
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-9900
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-10023
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-10864
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-15420
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-15577
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-15785
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-15832
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-16305
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-16327
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-37345
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-41222
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-40341
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-40409
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-40513
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
-1
View File
@@ -1 +0,0 @@
Release builds and development builds will reside here.
+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
@@ -1 +1 @@
../../../dist/browser-matrix-dev.js
../../../dist/browser-matrix.js
+9 -5
View File
@@ -135,11 +135,15 @@ rl.on('line', function(line) {
// ==== END User input
// show the room list after syncing.
matrixClient.on("syncComplete", function() {
setRoomList();
printRoomList();
printHelp();
rl.prompt();
matrixClient.on("sync", function(state, prevState, data) {
switch (state) {
case "PREPARED":
setRoomList();
printRoomList();
printHelp();
rl.prompt();
break;
}
});
matrixClient.on("Room", function() {
+10 -2
View File
@@ -44,7 +44,15 @@ window.onload = function() {
disableButtons(true, true, true);
};
client.on("syncComplete", function () {
matrixClient.on("sync", function(state, prevState, data) {
switch (state) {
case "PREPARED":
syncComplete();
break;
}
});
function syncComplete() {
document.getElementById("result").innerHTML = "<p>Ready for calls.</p>";
disableButtons(false, true, true);
@@ -85,5 +93,5 @@ client.on("syncComplete", function () {
call = c;
addListeners(call);
});
});
}
client.startClient();
+1 -1
View File
@@ -1 +1 @@
../../../dist/browser-matrix-dev.js
../../../dist/browser-matrix.js
+9 -9
View File
@@ -1,9 +1,12 @@
#!/bin/bash -l
set -x
export NVM_DIR="/home/jenkins/.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
@@ -14,13 +17,8 @@ function fail {
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
@@ -28,4 +26,6 @@ rm -f matrix-js-sdk-*.tgz
npm pack ||
fail "npm pack finished with return code $?"
npm run gendoc || fail "JSDoc failed with code $?"
exit $RC
-458
View File
@@ -1,458 +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";
/**
* Defines m.olm encryption/decryption
*
* @module crypto/algorithms/megolm
*/
var q = require("q");
var utils = require("../../utils");
var olmlib = require("../olmlib");
var base = require("./base");
/**
* Megolm encryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.EncryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.EncryptionAlgorithm}
*/
function MegolmEncryption(params) {
base.EncryptionAlgorithm.call(this, params);
this._prepPromise = null;
this._outboundSessionId = null;
this._discardNewSession = false;
// devices which have joined since we last sent a message.
// userId -> {deviceId -> true}, or
// userId -> true
this._devicesPendingKeyShare = {};
this._sharePromise = null;
}
utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
/**
* @private
*
* @param {module:models/room} room
*
* @return {module:client.Promise} Promise which resolves to the megolm
* sessionId when setup is complete.
*/
MegolmEncryption.prototype._ensureOutboundSession = function(room) {
var self = this;
if (this._prepPromise) {
// prep already in progress
return this._prepPromise;
}
var sessionId = this._outboundSessionId;
// need to make a brand new session?
if (!sessionId) {
this._prepPromise = this._prepareNewSession(room).
finally(function() {
self._prepPromise = null;
});
return this._prepPromise;
}
if (this._sharePromise) {
// key share already in progress
return this._sharePromise;
}
// prep already done, but check for new devices
var shareMap = this._devicesPendingKeyShare;
this._devicesPendingKeyShare = {};
// check each user is (still) a member of the room
for (var userId in shareMap) {
if (!shareMap.hasOwnProperty(userId)) {
continue;
}
// XXX what about rooms where invitees can see the content?
var member = room.getMember(userId);
if (member.membership !== "join") {
delete shareMap[userId];
}
}
this._sharePromise = this._shareKeyWithDevices(
sessionId, shareMap
).finally(function() {
self._sharePromise = null;
}).then(function() {
return sessionId;
});
return this._sharePromise;
};
/**
* @private
*
* @param {module:models/room} room
*
* @return {module:client.Promise} Promise which resolves to the megolm
* sessionId when setup is complete.
*/
MegolmEncryption.prototype._prepareNewSession = function(room) {
var session_id = this._olmDevice.createOutboundGroupSession();
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
this._olmDevice.addInboundGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, session_id,
key.key, {ed25519: this._olmDevice.deviceEd25519Key}
);
// we're going to share the key with all current members of the room,
// so we can reset this.
this._devicesPendingKeyShare = {};
var roomMembers = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
var shareMap = {};
for (var i = 0; i < roomMembers.length; i++) {
var userId = roomMembers[i];
shareMap[userId] = true;
}
var self = this;
// TODO: we need to give the user a chance to block any devices or users
// before we send them the keys; it's too late to download them here.
return this._crypto.downloadKeys(
roomMembers, false
).then(function(res) {
return self._shareKeyWithDevices(session_id, shareMap);
}).then(function() {
if (self._discardNewSession) {
// we've had cause to reset the session_id since starting this process.
// we'll use the current session for any currently pending events, but
// don't save it as the current _outboundSessionId, so that new events
// will use a new session.
console.log("Session generation complete, but discarding");
} else {
self._outboundSessionId = session_id;
}
return session_id;
}).finally(function() {
self._discardNewSession = false;
});
};
/**
* @private
*
* @param {string} session_id
*
* @param {Object<string, Object<string, boolean>|boolean>} shareMap
* Map from userid to either: true (meaning this is a new user in the room,
* so all of his devices need the keys); or a map from deviceid to true
* (meaning this user has one or more new devices, which need the keys).
*
* @return {module:client.Promise} Promise which resolves once the key sharing
* message has been sent.
*/
MegolmEncryption.prototype._shareKeyWithDevices = function(session_id, shareMap) {
var self = this;
var key = this._olmDevice.getOutboundGroupSessionKey(session_id);
var payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: session_id,
session_key: key.key,
chain_index: key.chain_index,
}
};
// we downloaded the user's device list when they joined the room, or when
// the new device announced itself, so there is no need to do so now.
return self._crypto.ensureOlmSessionsForUsers(
utils.keys(shareMap)
).then(function(devicemap) {
var contentMap = {};
var haveTargets = false;
for (var userId in devicemap) {
if (!devicemap.hasOwnProperty(userId)) {
continue;
}
var devicesToShareWith = shareMap[userId];
var sessionResults = devicemap[userId];
for (var deviceId in sessionResults) {
if (!sessionResults.hasOwnProperty(deviceId)) {
continue;
}
if (devicesToShareWith === true) {
// all devices
} else if (!devicesToShareWith[deviceId]) {
// not a new device
continue;
}
var sessionResult = sessionResults[deviceId];
if (!sessionResult.sessionId) {
// no session with this device, probably because there
// were no one-time keys.
//
// we could send them a to_device message anyway, as a
// signal that they have missed out on the key sharing
// message because of the lack of keys, but there's not
// much point in that really; it will mostly serve to clog
// up to_device inboxes.
//
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
continue;
}
console.log(
"sharing keys with device " + userId + ":" + deviceId
);
var deviceInfo = sessionResult.device;
if (!contentMap[userId]) {
contentMap[userId] = {};
}
contentMap[userId][deviceId] =
olmlib.encryptMessageForDevices(
self._deviceId,
self._olmDevice,
[deviceInfo.getIdentityKey()],
payload
);
haveTargets = true;
}
}
if (!haveTargets) {
return q();
}
// TODO: retries
return self._baseApis.sendToDevice("m.room.encrypted", contentMap);
});
};
/**
* @inheritdoc
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
var self = this;
return this._ensureOutboundSession(room).then(function(session_id) {
var payloadJson = {
room_id: self._roomId,
type: eventType,
content: content
};
var ciphertext = self._olmDevice.encryptGroupMessage(
session_id, JSON.stringify(payloadJson)
);
var encryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext,
session_id: session_id,
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
device_id: self._deviceId,
};
return encryptedContent;
});
};
/**
* @inheritdoc
*
* @param {module:models/event.MatrixEvent} event event causing the change
* @param {module:models/room-member} member user whose membership changed
* @param {string=} oldMembership previous membership
*/
MegolmEncryption.prototype.onRoomMembership = function(event, member, oldMembership) {
var newMembership = member.membership;
if (newMembership === 'join') {
this._onNewRoomMember(member.userId);
return;
}
if (newMembership === 'invite' && oldMembership !== 'join') {
// we don't (yet) share keys with invited members, so nothing to do yet
return;
}
// otherwise we assume the user is leaving, and start a new outbound session.
if (this._outboundSessionId) {
console.log("Discarding outbound megolm session due to change in " +
"membership of " + member.userId + " (" + oldMembership +
"->" + newMembership + ")");
this._outboundSessionId = null;
}
if (this._prepPromise) {
console.log("Discarding as-yet-incomplete megolm session due to " +
"change in membership of " + member.userId + " (" +
oldMembership + "->" + newMembership + ")");
this._discardNewSession = true;
}
};
/**
* handle a new user joining a room
*
* @param {string} userId new member
*/
MegolmEncryption.prototype._onNewRoomMember = function(userId) {
// make sure we have a list of this user's devices. We are happy to use a
// cached version here: we assume that if we already have a list of the
// user's devices, then we already share an e2e room with them, which means
// that they will have announced any new devices via an m.new_device.
this._crypto.downloadKeys([userId], false).done();
// also flag this user up for needing a keyshare.
this._devicesPendingKeyShare[userId] = true;
};
/**
* @inheritdoc
*
* @param {string} userId owner of the device
* @param {string} deviceId deviceId of the device
*/
MegolmEncryption.prototype.onNewDevice = function(userId, deviceId) {
var d = this._devicesPendingKeyShare[userId];
if (d === true) {
// we already want to share keys with all devices for this user
return;
}
if (!d) {
this._devicesPendingKeyShare[userId] = d = {};
}
d[deviceId] = true;
};
/**
* Megolm decryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.DecryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.DecryptionAlgorithm}
*/
function MegolmDecryption(params) {
base.DecryptionAlgorithm.call(this, params);
}
utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
/**
* @inheritdoc
*
* @param {object} event raw event
*
* @return {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.content;
if (!content.sender_key || !content.session_id ||
!content.ciphertext
) {
throw new base.DecryptionError("Missing fields in input");
}
try {
var res = this._olmDevice.decryptGroupMessage(
event.room_id, content.sender_key, content.session_id, content.ciphertext
);
if (res === null) {
return null;
}
return {
payload: JSON.parse(res.result),
keysClaimed: res.keysClaimed,
keysProved: res.keysProved,
};
} catch (e) {
throw new base.DecryptionError(e);
}
};
/**
* @inheritdoc
*
* @param {module:models/event.MatrixEvent} event key event
*/
MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
console.log("Adding key from ", event);
var content = event.getContent();
if (!content.room_id ||
!content.session_id ||
!content.session_key
) {
console.error("key event is missing fields");
return;
}
this._olmDevice.addInboundGroupSession(
content.room_id, event.getSenderKey(), content.session_id,
content.session_key, event.getKeysClaimed()
);
};
base.registerAlgorithm(
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption
);
-90
View File
@@ -1,90 +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.
*/
/**
* @module olmlib
*
* Utilities common to olm encryption algorithms
*/
var utils = require("../utils");
/**
* matrix algorithm tag for olm
*/
module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
/**
* matrix algorithm tag for megolm
*/
module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
/**
* Encrypt an event payload for a list of devices
*
* @param {string} ourDeviceId
* @param {module:crypto/OlmDevice} olmDevice olm.js wrapper
* @param {string[]} participantKeys list of curve25519 keys to encrypt for
* @param {object} payloadFields fields to include in the encrypted payload
*
* @return {object} content for an m.room.encrypted event
*/
module.exports.encryptMessageForDevices = function(
ourDeviceId, olmDevice, participantKeys, payloadFields
) {
participantKeys.sort();
var participantHash = ""; // Olm.sha256(participantKeys.join());
var payloadJson = {
fingerprint: participantHash,
sender_device: ourDeviceId,
// Include the Ed25519 key so that the recipient knows what
// device this message came from.
// We don't need to include the curve25519 key since the
// recipient will already know this from the olm headers.
// When combined with the device keys retrieved from the
// homeserver signed by the ed25519 key this proves that
// the curve25519 key and the ed25519 key are owned by
// the same device.
keys: {
"ed25519": olmDevice.deviceEd25519Key,
},
};
utils.extend(payloadJson, payloadFields);
var ciphertext = {};
var payloadString = JSON.stringify(payloadJson);
for (var i = 0; i < participantKeys.length; ++i) {
var deviceKey = participantKeys[i];
var sessionId = olmDevice.getSessionIdForDevice(deviceKey);
if (sessionId === null) {
// If we don't have a session for a device then
// we can't encrypt a message for it.
continue;
}
console.log("Using sessionid " + sessionId + " for device " + deviceKey);
ciphertext[deviceKey] = olmDevice.encryptMessage(
deviceKey, sessionId, payloadString
);
}
var encryptedContent = {
algorithm: module.exports.OLM_ALGORITHM,
sender_key: olmDevice.deviceCurve25519Key,
ciphertext: ciphertext
};
return encryptedContent;
};
-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;
+53 -14
View File
@@ -1,18 +1,19 @@
{
"name": "matrix-js-sdk",
"version": "0.6.2",
"version": "0.7.5-rc.1",
"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",
"buildtest": "babel -s -d specbuild spec",
"test": "npm run buildtest && istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- specbuild --verbose --junitreport --captureExceptions",
"check": "npm run buildtest && jasmine-node specbuild --verbose --junitreport --captureExceptions",
"gendoc": "jsdoc -r lib -P package.json -R README.md -d .jsdoc",
"build": "jshint -c .jshint lib/ && browserify browser-index.js -o dist/browser-matrix-dev.js --ignore-missing",
"watch": "watchify browser-index.js -o dist/browser-matrix-dev.js -v",
"lint": "jshint -c .jshint lib spec && gjslint --unix_mode --disable 0131,0211,0200,0222,0212 --max_line_length 90 -r spec/ -r lib/",
"release": "npm run build && mkdir -p dist/$npm_package_version && uglifyjs -c -m -o dist/$npm_package_version/browser-matrix-$npm_package_version.min.js dist/browser-matrix-dev.js && cp dist/browser-matrix-dev.js dist/$npm_package_version/browser-matrix-$npm_package_version.js",
"prepublish": "git rev-parse HEAD > git-revision.txt",
"version": "npm run release && git add dist/$npm_package_version"
"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 -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
"lint": "eslint --max-warnings 121 src spec",
"prepublish": "npm run build && git rev-parse HEAD > git-revision.txt"
},
"repository": {
"url": "https://github.com/matrix-org/matrix-js-sdk"
@@ -23,22 +24,60 @@
"browser": "browser-index.js",
"author": "matrix.org",
"license": "Apache-2.0",
"files": [
".babelrc",
".eslintrc.js",
"spec/.eslintrc.js",
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
"README.md",
"RELEASING.md",
"examples",
"git-hooks",
"git-revision.txt",
"index.js",
"browser-index.js",
"jenkins.sh",
"lib",
"package.json",
"release.sh",
"spec",
"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": {
"watchify": "^3.2.1",
"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",
"istanbul": "^0.3.13",
"jasmine-node": "^1.14.5",
"jshint": "^2.8.0",
"jsdoc": "^3.4.0",
"uglifyjs": "^2.4.10"
"rimraf": "^2.5.4",
"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"
}
}
+128 -10
View File
@@ -1,15 +1,18 @@
#!/bin/sh
#!/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
jq --version > /dev/null || (echo "jq is required: please install it"; kill $$)
hub --version > /dev/null || (echo "hub is required: please install it"; kill $$)
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
help() {
@@ -29,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"
@@ -56,11 +69,27 @@ if [ $# -ne 1 ]; then
exit 1
fi
if [ -z "$skip_changelog" ]; then
# update_changelog doesn't have a --version flag
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
fi
# ignore leading v on release
release="${1#v}"
tag="v${release}"
rel_branch="release-$tag"
prerelease=0
# We check if this build is a prerelease by looking to
# see if the version has a hyphen in it. Crude,
# but semver doesn't support postreleases so anything
# with a hyphen is a prerelease.
echo $release | grep -q '-' && prerelease=1
if [ $prerelease -eq 1 ]; then
echo Making a PRE-RELEASE
fi
if [ -z "$skip_changelog" ]; then
if ! command -v update_changelog >/dev/null 2>&1; then
echo "release.sh requires github-changelog-generator. Try:" >&2
@@ -70,9 +99,16 @@ if [ -z "$skip_changelog" ]; then
fi
# we might already be on the release branch, in which case, yay
if [ $(git symbolic-ref --short HEAD) != "$rel_branch" ]; then
# If we're on any branch starting with 'release', we don't create
# a separate release branch (this allows us to use the same
# release branch for releases and release candidates).
curbranch=$(git symbolic-ref --short HEAD)
if [[ "$curbranch" != release* ]]; then
echo "Creating release branch"
git checkout -b "$rel_branch"
else
echo "Using current branch ($curbranch) for release"
rel_branch=$curbranch
fi
if [ -z "$skip_changelog" ]; then
@@ -85,12 +121,94 @@ 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 "$release"
# npm version will automatically commit its modification
# and make a release tag. We don't want it to create the tag
# because it can only sign with the default key, but we can
# only turn off both of these behaviours, so we have to
# manually commit the result.
npm version --no-git-tag-version "$release"
git commit package.json -m "$tag"
# figure out if we should be signing this release
signing_id=
if [ -f release_config.yaml ]; then
signing_id=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']"`
fi
# If there is a 'dist' script in the package.json,
# run it in a separate checkout of the project, then
# upload any files in the 'dist' directory as release
# assets.
# We make a completely separate checkout to be sure
# we're using released versions of the dependencies
# (rather than whatever we're pulling in from npm link)
assets=''
dodist=0
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
if [ $dodist -eq 0 ]; then
projdir=`pwd`
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
echo "Building distribution copy in $builddir"
pushd "$builddir"
git clone "$projdir" .
git checkout "$rel_branch"
npm install
# We haven't tagged yet, so tell the dist script what version
# it's building
DIST_VERSION="$tag" npm run dist
popd
for i in "$builddir"/dist/*; do
assets="$assets -a $i"
if [ -n "$signing_id" ]
then
gpg -u "$signing_id" --armor --output "$i".asc --detach-sig "$i"
assets="$assets -a $i.asc"
fi
done
fi
# push the release branch (github can't release from
# a branch it doesn't have)
git push origin "$rel_branch"
if [ -n "$signing_id" ]; then
# make a signed tag
# gnupg seems to fail to get the right tty device unless we set it here
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
else
git tag -a -F "${latest_changes}" "$tag"
fi
# push the tag
git push origin "$tag"
hubflags=''
if [ $prerelease -eq 1 ]; then
hubflags='-p'
fi
release_text=`mktemp`
echo "$tag" > "${release_text}"
echo >> "${release_text}"
cat "${latest_changes}" >> "${release_text}"
hub release create $hubflags $assets -f "${release_text}" "$tag"
if [ $dodist -eq 0 ]; then
rm -rf "$builddir"
fi
rm "${release_text}"
rm "${latest_changes}"
if [ -z "$skip_jsdoc" ]; then
echo "generating jsdocs"
@@ -113,8 +231,8 @@ git checkout master
git pull
git merge --ff-only "$rel_branch"
# push everything to github
git push origin master "$rel_branch" "$tag"
# push master and docs (if generated) to github
git push origin master
if [ -z "$skip_jsdoc" ]; then
git push origin gh-pages
fi
+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: {
jasmine: 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;
}
},
};
/** */
+290 -110
View File
@@ -1,46 +1,30 @@
"use strict";
var sdk = require("../..");
var q = require("q");
var HttpBackend = require("../mock-request");
var utils = require("../../lib/utils");
var test_utils = require("../test-utils");
const sdk = require("../..");
const q = require("q");
const HttpBackend = require("../mock-request");
const utils = require("../../lib/utils");
const testUtils = require("../test-utils");
function MockStorageApi() {
this.data = {};
}
MockStorageApi.prototype = {
setItem: function(k, v) {
this.data[k] = v;
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
}
};
var aliHttpBackend;
var bobHttpBackend;
var aliClient;
var roomId = "!room:localhost";
var aliUserId = "@ali:localhost";
var aliDeviceId = "zxcvb";
var aliAccessToken = "aseukfgwef";
var bobClient;
var bobUserId = "@bob:localhost";
var bobDeviceId = "bvcxz";
var bobAccessToken = "fewgfkuesa";
var bobOneTimeKeys;
var aliDeviceKeys;
var bobDeviceKeys;
var bobDeviceCurve25519Key;
var bobDeviceEd25519Key;
var aliLocalStore;
var aliStorage;
var bobStorage;
var aliMessages;
var bobMessages;
let aliHttpBackend;
let bobHttpBackend;
let aliClient;
const roomId = "!room:localhost";
const aliUserId = "@ali:localhost";
const aliDeviceId = "zxcvb";
const aliAccessToken = "aseukfgwef";
let bobClient;
const bobUserId = "@bob:localhost";
const bobDeviceId = "bvcxz";
const bobAccessToken = "fewgfkuesa";
let bobOneTimeKeys;
let aliDeviceKeys;
let bobDeviceKeys;
let bobDeviceCurve25519Key;
let bobDeviceEd25519Key;
let aliStorage;
let bobStorage;
let aliMessages;
let bobMessages;
/**
@@ -54,29 +38,29 @@ var bobMessages;
* {one_time_keys: {}, device_keys: {}}
*/
function expectKeyUpload(deviceId, httpBackend) {
var uploadPath = "/keys/upload/" + deviceId;
var keys = {};
const uploadPath = "/keys/upload/" + deviceId;
const keys = {};
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.one_time_keys).not.toBeDefined();
expect(content.device_keys).toBeDefined();
keys.device_keys = content.device_keys;
return {one_time_key_counts: {curve25519: 0}};
return {one_time_key_counts: {signed_curve25519: 0}};
});
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.device_keys).not.toBeDefined();
expect(content.one_time_keys).toBeDefined();
expect(content.one_time_keys).not.toEqual({});
var count = 0;
for (var key in content.one_time_keys) {
let count = 0;
for (const key in content.one_time_keys) {
if (content.one_time_keys.hasOwnProperty(key)) {
count++;
}
}
expect(count).toEqual(5);
keys.one_time_keys = content.one_time_keys;
return {one_time_key_counts: {curve25519: count}};
return {one_time_key_counts: {signed_curve25519: count}};
});
return httpBackend.flush(uploadPath, 2).then(function() {
@@ -121,7 +105,7 @@ function expectBobKeyUpload() {
}
function bobUploadsKeys() {
bobClient.uploadKeys(5).catch(test_utils.failTest);
bobClient.uploadKeys(5).catch(testUtils.failTest);
return expectBobKeyUpload();
}
@@ -135,11 +119,11 @@ function expectAliQueryKeys() {
// can't query keys before bob has uploaded them
expect(bobDeviceKeys).toBeDefined();
var bobKeys = {};
const bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual({});
var result = {};
const result = {};
result[bobUserId] = bobKeys;
return {device_keys: result};
});
@@ -155,11 +139,11 @@ function expectBobQueryKeys() {
// can't query keys before ali has uploaded them
expect(aliDeviceKeys).toBeDefined();
var aliKeys = {};
const aliKeys = {};
aliKeys[aliDeviceId] = aliDeviceKeys;
bobHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[aliUserId]).toEqual({});
var result = {};
const result = {};
result[aliUserId] = aliKeys;
return {device_keys: result};
});
@@ -176,15 +160,17 @@ function expectAliClaimKeys() {
expect(bobOneTimeKeys).toBeDefined();
aliHttpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
expect(content.one_time_keys[bobUserId][bobDeviceId]).toEqual("curve25519");
for (var keyId in bobOneTimeKeys) {
const claimType = content.one_time_keys[bobUserId][bobDeviceId];
expect(claimType).toEqual("signed_curve25519");
let keyId = null;
for (keyId in bobOneTimeKeys) {
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
if (keyId.indexOf("curve25519:") === 0) {
if (keyId.indexOf(claimType + ":") === 0) {
break;
}
}
}
var result = {};
const result = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = bobOneTimeKeys[keyId];
@@ -199,7 +185,7 @@ function aliDownloadsKeys() {
// can't query keys before bob has uploaded them
expect(bobDeviceEd25519Key).toBeDefined();
var p1 = aliClient.downloadKeys([bobUserId]).then(function() {
const p1 = aliClient.downloadKeys([bobUserId]).then(function() {
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([{
id: "bvcxz",
key: bobDeviceEd25519Key,
@@ -208,12 +194,12 @@ function aliDownloadsKeys() {
display_name: null,
}]);
});
var p2 = expectAliQueryKeys();
const p2 = expectAliQueryKeys();
// check that the localStorage is updated as we expect (not sure this is
// an integration test, but meh)
return q.all([p1, p2]).then(function() {
var devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
const devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
expect(devices[bobDeviceId].keys).toEqual(bobDeviceKeys.keys);
expect(devices[bobDeviceId].verified).
toBe(0); // DeviceVerification.UNVERIFIED
@@ -247,7 +233,7 @@ function aliSendsFirstMessage() {
sendMessage(aliClient),
expectAliQueryKeys()
.then(expectAliClaimKeys)
.then(expectAliSendMessageRequest)
.then(expectAliSendMessageRequest),
]).spread(function(_, ciphertext) {
return ciphertext;
});
@@ -262,7 +248,7 @@ function aliSendsFirstMessage() {
function aliSendsMessage() {
return q.all([
sendMessage(aliClient),
expectAliSendMessageRequest()
expectAliSendMessageRequest(),
]).spread(function(_, ciphertext) {
return ciphertext;
});
@@ -278,7 +264,7 @@ function bobSendsReplyMessage() {
return q.all([
sendMessage(bobClient),
expectBobQueryKeys()
.then(expectBobSendMessageRequest)
.then(expectBobSendMessageRequest),
]).spread(function(_, ciphertext) {
return ciphertext;
});
@@ -293,7 +279,7 @@ function expectAliSendMessageRequest() {
return expectSendMessageRequest(aliHttpBackend).then(function(content) {
aliMessages.push(content);
expect(utils.keys(content.ciphertext)).toEqual([bobDeviceCurve25519Key]);
var ciphertext = content.ciphertext[bobDeviceCurve25519Key];
const ciphertext = content.ciphertext[bobDeviceCurve25519Key];
expect(ciphertext).toBeDefined();
return ciphertext;
});
@@ -307,10 +293,10 @@ function expectAliSendMessageRequest() {
function expectBobSendMessageRequest() {
return expectSendMessageRequest(bobHttpBackend).then(function(content) {
bobMessages.push(content);
var aliKeyId = "curve25519:" + aliDeviceId;
var aliDeviceCurve25519Key = aliDeviceKeys.keys[aliKeyId];
const aliKeyId = "curve25519:" + aliDeviceId;
const aliDeviceCurve25519Key = aliDeviceKeys.keys[aliKeyId];
expect(utils.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
var ciphertext = content.ciphertext[aliDeviceCurve25519Key];
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
expect(ciphertext).toBeDefined();
return ciphertext;
});
@@ -318,13 +304,13 @@ function expectBobSendMessageRequest() {
function sendMessage(client) {
return client.sendMessage(
roomId, {msgtype: "m.text", body: "Hello, World"}
roomId, {msgtype: "m.text", body: "Hello, World"},
);
}
function expectSendMessageRequest(httpBackend) {
var path = "/send/m.room.encrypted/";
var sent;
const path = "/send/m.room.encrypted/";
let sent;
httpBackend.when("PUT", path).respond(200, function(path, content) {
sent = content;
return {
@@ -337,38 +323,39 @@ function expectSendMessageRequest(httpBackend) {
}
function aliRecvMessage() {
var message = bobMessages.shift();
return recvMessage(aliHttpBackend, aliClient, message);
const message = bobMessages.shift();
return recvMessage(aliHttpBackend, aliClient, bobUserId, message);
}
function bobRecvMessage() {
var message = aliMessages.shift();
return recvMessage(bobHttpBackend, bobClient, message);
const message = aliMessages.shift();
return recvMessage(bobHttpBackend, bobClient, aliUserId, message);
}
function recvMessage(httpBackend, client, message) {
var syncData = {
function recvMessage(httpBackend, client, sender, message) {
const syncData = {
next_batch: "x",
rooms: {
join: {
}
}
},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
test_utils.mkEvent({
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message
})
]
}
content: message,
sender: sender,
}),
],
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
var deferred = q.defer();
var onEvent = function(event) {
const deferred = q.defer();
const onEvent = function(event) {
console.log(client.credentials.userId + " received event",
event);
@@ -380,7 +367,7 @@ function recvMessage(httpBackend, client, message) {
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toEqual({
msgtype: "m.text",
body: "Hello, World"
body: "Hello, World",
});
expect(event.isEncrypted()).toBeTruthy();
@@ -396,7 +383,16 @@ function recvMessage(httpBackend, client, message) {
function aliStartClient() {
expectAliKeyUpload().catch(test_utils.failTest);
expectAliKeyUpload().catch(testUtils.failTest);
// ali will try to query her own keys on start
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[aliUserId]).toEqual({});
const result = {};
result[aliUserId] = {};
return {device_keys: result};
});
startClient(aliHttpBackend, aliClient);
return aliHttpBackend.flush().then(function() {
console.log("Ali client started");
@@ -404,7 +400,16 @@ function aliStartClient() {
}
function bobStartClient() {
expectBobKeyUpload().catch(test_utils.failTest);
expectBobKeyUpload().catch(testUtils.failTest);
// bob will try to query his own keys on start
bobHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual({});
const result = {};
result[bobUserId] = {};
return {device_keys: result};
});
startClient(bobHttpBackend, bobClient);
return bobHttpBackend.flush().then(function() {
console.log("Bob client started");
@@ -424,28 +429,28 @@ function startClient(httpBackend, client) {
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
// send a sync response including our test room.
var syncData = {
const syncData = {
next_batch: "x",
rooms: {
join: { }
}
join: { },
},
};
syncData.rooms.join[roomId] = {
state: {
events: [
test_utils.mkMembership({
testUtils.mkMembership({
mship: "join",
user: aliUserId,
}),
test_utils.mkMembership({
testUtils.mkMembership({
mship: "join",
user: bobUserId,
}),
]
],
},
timeline: {
events: []
}
events: [],
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
@@ -459,11 +464,9 @@ describe("MatrixClient crypto", function() {
}
beforeEach(function() {
aliLocalStore = new MockStorageApi();
aliStorage = new sdk.WebStorageSessionStore(aliLocalStore);
bobStorage = new sdk.WebStorageSessionStore(new MockStorageApi());
test_utils.beforeEach(this);
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
aliStorage = new sdk.WebStorageSessionStore(new testUtils.MockStorageApi());
aliHttpBackend = new HttpBackend();
aliClient = sdk.createClient({
baseUrl: "http://alis.server",
@@ -474,6 +477,7 @@ describe("MatrixClient crypto", function() {
request: aliHttpBackend.requestFn,
});
bobStorage = new sdk.WebStorageSessionStore(new testUtils.MockStorageApi());
bobHttpBackend = new HttpBackend();
bobClient = sdk.createClient({
baseUrl: "http://bobs.server",
@@ -498,17 +502,38 @@ describe("MatrixClient crypto", function() {
bobClient.stopClient();
});
it('Ali knows the difference between a new user and one with no devices',
function(done) {
aliHttpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:id': {},
},
});
const p1 = aliClient.downloadKeys(['@bob:id']);
const p2 = aliHttpBackend.flush('/keys/query', 1);
q.all([p1, p2]).then(function() {
const devices = aliStorage.getEndToEndDevicesForUser('@bob:id');
expect(utils.keys(devices).length).toEqual(0);
// request again: should be no more requests
return aliClient.downloadKeys(['@bob:id']);
}).nodeify(done);
},
);
it("Bob uploads without one-time keys and with one-time keys", function(done) {
q()
.then(bobUploadsKeys)
.catch(test_utils.failTest).done(done);
.catch(testUtils.failTest).done(done);
});
it("Ali downloads Bobs keys", function(done) {
q()
.then(bobUploadsKeys)
.then(aliDownloadsKeys)
.catch(test_utils.failTest).done(done);
.catch(testUtils.failTest).done(done);
});
it("Ali gets keys with an invalid signature", function(done) {
@@ -526,7 +551,78 @@ describe("MatrixClient crypto", function() {
// should get an empty list
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
})
.catch(test_utils.failTest).done(done);
.catch(testUtils.failTest).done(done);
});
it("Ali gets keys with an incorrect userId", function(done) {
const eveUserId = "@eve:localhost";
const bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'bvcxz',
keys: {
'ed25519:bvcxz': 'pYuWKMCVuaDLRTM/eWuB8OlXEb61gZhfLVJ+Y54tl0Q',
'curve25519:bvcxz': '7Gni0loo/nzF0nFp9847RbhElGewzwUXHPrljjBGPTQ',
},
user_id: '@eve:localhost',
signatures: {
'@eve:localhost': {
'ed25519:bvcxz': 'CliUPZ7dyVPBxvhSA1d+X+LYa5b2AYdjcTwG' +
'0stXcIxjaJNemQqtdgwKDtBFl3pN2I13SEijRDCf1A8bYiQMDg',
},
},
};
const bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
const result = {};
result[bobUserId] = bobKeys;
return {device_keys: result};
});
q.all(
aliClient.downloadKeys([bobUserId, eveUserId]),
aliHttpBackend.flush("/keys/query", 1),
).then(function() {
// should get an empty list
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
expect(aliClient.listDeviceKeys(eveUserId)).toEqual([]);
}).catch(testUtils.failTest).done(done);
});
it("Ali gets keys with an incorrect deviceId", function(done) {
const bobDeviceKeys = {
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
device_id: 'bad_device',
keys: {
'ed25519:bad_device': 'e8XlY5V8x2yJcwa5xpSzeC/QVOrU+D5qBgyTK0ko+f0',
'curve25519:bad_device': 'YxuuLG/4L5xGeP8XPl5h0d7DzyYVcof7J7do+OXz0xc',
},
user_id: '@bob:localhost',
signatures: {
'@bob:localhost': {
'ed25519:bad_device': 'fEFTq67RaSoIEVBJ8DtmRovbwUBKJ0A' +
'me9m9PDzM9azPUwZ38Xvf6vv1A7W1PSafH4z3Y2ORIyEnZgHaNby3CQ',
},
},
};
const bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
const result = {};
result[bobUserId] = bobKeys;
return {device_keys: result};
});
q.all(
aliClient.downloadKeys([bobUserId]),
aliHttpBackend.flush("/keys/query", 1),
).then(function() {
// should get an empty list
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
}).catch(testUtils.failTest).done(done);
});
it("Ali enables encryption", function(done) {
@@ -534,7 +630,7 @@ describe("MatrixClient crypto", function() {
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.catch(test_utils.failTest).done(done);
.catch(testUtils.failTest).done(done);
});
it("Ali sends a message", function(done) {
@@ -543,7 +639,7 @@ describe("MatrixClient crypto", function() {
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.catch(test_utils.failTest).nodeify(done);
.catch(testUtils.failTest).nodeify(done);
});
it("Bob receives a message", function(done) {
@@ -554,7 +650,64 @@ describe("MatrixClient crypto", function() {
.then(aliSendsFirstMessage)
.then(bobStartClient)
.then(bobRecvMessage)
.catch(test_utils.failTest).done(done);
.catch(testUtils.failTest).done(done);
});
it("Bob receives a message with a bogus sender", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobStartClient)
.then(function() {
const message = aliMessages.shift();
const syncData = {
next_batch: "x",
rooms: {
join: {
},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: "@bogus:sender",
}),
],
},
};
bobHttpBackend.when("GET", "/sync").respond(200, syncData);
const deferred = q.defer();
const onEvent = function(event) {
console.log(bobClient.credentials.userId + " received event",
event);
// ignore the m.room.member events
if (event.getType() == "m.room.member") {
return;
}
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
expect(event.isEncrypted()).toBeTruthy();
bobClient.removeListener("event", onEvent);
deferred.resolve();
};
bobClient.on("event", onEvent);
bobHttpBackend.flush();
return deferred.promise;
})
.catch(testUtils.failTest).done(done);
});
it("Ali blocks Bob's device", function(done) {
@@ -565,8 +718,8 @@ describe("MatrixClient crypto", function() {
.then(aliDownloadsKeys)
.then(function() {
aliClient.setDeviceBlocked(bobUserId, bobDeviceId, true);
var p1 = sendMessage(aliClient);
var p2 = expectAliQueryKeys()
const p1 = sendMessage(aliClient);
const p2 = expectAliQueryKeys()
.then(expectAliClaimKeys)
.then(function() {
return expectSendMessageRequest(aliHttpBackend);
@@ -575,7 +728,7 @@ describe("MatrixClient crypto", function() {
expect(sentContent.ciphertext).toEqual({});
});
return q.all([p1, p2]);
}).catch(test_utils.failTest).nodeify(done);
}).catch(testUtils.failTest).nodeify(done);
});
it("Bob receives two pre-key messages", function(done) {
@@ -588,7 +741,7 @@ describe("MatrixClient crypto", function() {
.then(bobRecvMessage)
.then(aliSendsMessage)
.then(bobRecvMessage)
.catch(test_utils.failTest).done(done);
.catch(testUtils.failTest).done(done);
});
it("Bob replies to the message", function(done) {
@@ -603,6 +756,33 @@ describe("MatrixClient crypto", function() {
.then(bobSendsReplyMessage).then(function(ciphertext) {
expect(ciphertext.type).toEqual(1);
}).then(aliRecvMessage)
.catch(test_utils.failTest).done(done);
.catch(testUtils.failTest).done(done);
});
it("Ali does a key query when she gets a new_device event", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(function() {
const syncData = {
next_batch: '2',
to_device: {
events: [
testUtils.mkEvent({
content: {
device_id: 'TEST_DEVICE',
rooms: [],
},
sender: bobUserId,
type: 'm.new_device',
}),
],
},
};
aliHttpBackend.when('GET', '/sync').respond(200, syncData);
return aliHttpBackend.flush('/sync', 1);
}).then(expectAliQueryKeys)
.nodeify(done);
});
});
+76 -73
View File
@@ -1,22 +1,23 @@
"use strict";
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const utils = require("../test-utils");
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 +29,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 +44,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 +75,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 +102,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 +121,7 @@ describe("MatrixClient events", function() {
}
}
expect(found).toBe(
true, "Unexpected 'event' emitted: " + event.getType()
true, "Unexpected 'event' emitted: " + event.getType(),
);
});
@@ -127,7 +129,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 +138,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; }
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 +163,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 +182,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 +198,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());
const index = roomStateEventTypes.indexOf(event.getType());
expect(index).not.toEqual(
-1, "Unexpected room state event type: " + event.getType()
-1, "Unexpected room state event type: " + event.getType(),
);
if (index >= 0) {
roomStateEventTypes.splice(index, 1);
@@ -227,13 +231,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 +247,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 +270,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 +288,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 +297,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();
});
});
});
});
+95 -92
View File
@@ -1,29 +1,29 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
var EventTimeline = sdk.EventTimeline;
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 +31,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 +81,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") {
@@ -96,11 +96,11 @@ function startClient(httpBackend, client) {
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");
}).not.toThrow();
}).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);
@@ -218,10 +220,11 @@ describe("getEventTimeline support", function() {
});
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 +246,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 +265,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 +279,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: {
@@ -308,8 +311,8 @@ describe("MatrixClient event timelines", function() {
});
it("should update timelines where they overlap a previous /sync", function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
@@ -340,7 +343,7 @@ describe("MatrixClient event timelines", function() {
});
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]);
@@ -358,8 +361,8 @@ describe("MatrixClient event timelines", function() {
it("should join timelines where they overlap a previous /context",
function(done) {
var room = client.getRoom(roomId);
var timelineSet = room.getTimelineSets()[0];
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 +418,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 +455,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 +470,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 +484,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 +502,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 +513,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 +535,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 +553,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 +564,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 +587,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 +607,7 @@ describe("MatrixClient event timelines", function() {
"!foo:bar": {
timeline: {
events: [
event
event,
],
prev_batch: "f_1_1",
},
@@ -615,8 +618,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);
@@ -639,8 +642,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.
@@ -672,18 +675,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 +704,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 +719,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 +730,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);
});
+211 -92
View File
@@ -1,25 +1,28 @@
"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");
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");
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);
@@ -37,14 +40,125 @@ describe("MatrixClient", function() {
httpBackend.verifyNoOutstandingExpectation();
});
describe("uploadContent", function() {
const buf = new Buffer('hello world');
it("should upload the file", function(done) {
httpBackend.when(
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.data).toEqual(buf);
expect(req.queryParams.filename).toEqual("hi.txt");
expect(req.queryParams.access_token).toEqual(accessToken);
expect(req.headers["Content-Type"]).toEqual("text/plain");
expect(req.opts.json).toBeFalsy();
expect(req.opts.timeout).toBe(undefined);
}).respond(200, "content");
const prom = client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
});
expect(prom).toBeDefined();
const uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(1);
expect(uploads[0].promise).toBe(prom);
expect(uploads[0].loaded).toEqual(0);
prom.then(function(response) {
// for backwards compatibility, we return the raw JSON
expect(response).toEqual("content");
const uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(0);
}).catch(utils.failTest).done(done);
httpBackend.flush();
});
it("should parse the response if rawResponse=false", function(done) {
httpBackend.when(
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.opts.json).toBeFalsy();
}).respond(200, JSON.stringify({ "content_uri": "uri" }));
client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
}, {
rawResponse: false,
}).then(function(response) {
expect(response.content_uri).toEqual("uri");
}).catch(utils.failTest).done(done);
httpBackend.flush();
});
it("should parse errors into a MatrixError", function(done) {
// opts.json is false, so request returns unparsed json.
httpBackend.when(
"POST", "/_matrix/media/v1/upload",
).check(function(req) {
expect(req.data).toEqual(buf);
expect(req.opts.json).toBeFalsy();
}).respond(400, JSON.stringify({
"errcode": "M_SNAFU",
"error": "broken",
}));
client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
}).then(function(response) {
throw Error("request not failed");
}, function(error) {
expect(error.httpStatus).toEqual(400);
expect(error.errcode).toEqual("M_SNAFU");
expect(error.message).toEqual("broken");
}).catch(utils.failTest).done(done);
httpBackend.flush();
});
it("should return a promise which can be cancelled", function(done) {
const prom = client.uploadContent({
stream: buf,
name: "hi.txt",
type: "text/plain",
});
const uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(1);
expect(uploads[0].promise).toBe(prom);
expect(uploads[0].loaded).toEqual(0);
prom.then(function(response) {
throw Error("request not aborted");
}, function(error) {
expect(error).toEqual("aborted");
const uploads = client.getCurrentUploads();
expect(uploads.length).toEqual(0);
}).catch(utils.failTest).done(done);
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);
@@ -53,11 +167,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) {
@@ -69,16 +183,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) {
@@ -91,13 +205,13 @@ 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();
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);
@@ -110,21 +224,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();
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) {
@@ -138,8 +252,7 @@ describe("MatrixClient", function() {
});
describe("searching", function() {
var response = {
const response = {
search_categories: {
room_events: {
count: 24,
@@ -152,26 +265,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);
@@ -184,58 +297,65 @@ describe("MatrixClient", function() {
describe("downloadKeys", function() {
it("should do an HTTP request and then store the keys", function(done) {
var ed25519key = "wV5E3EUSHpHuoZLljNzojlabjGdXT3Mz7rugG9zgbkI";
var borisKeys = {
const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
// ed25519key = client.getDeviceEd25519Key();
const borisKeys = {
dev1: {
algorithms: ["1"], keys: { "ed25519:dev1": ed25519key },
algorithms: ["1"],
device_id: "dev1",
keys: { "ed25519:dev1": ed25519key },
signatures: {
boris: {
"ed25519:dev1":
"u99n8WZ61G//K6eVgYc+RDLVapmjttxqhjNucIFGEIJ" +
"oA4TUY8FmiGv3zl0EA71zrvPDfnFL5XLNsdc55NGbDg"
}
"ed25519:dev1":
"RAhmbNDq1efK3hCpBzZDsKoGSsrHUxb25NW5/WbEV9R" +
"JVwLdP032mg5QsKt/pBDUGtggBcnk43n3nBWlA88WAw",
},
},
unsigned: { "abc": "def" },
}
user_id: "boris",
},
};
var chazKeys = {
const chazKeys = {
dev2: {
algorithms: ["2"], keys: { "ed25519:dev2": ed25519key },
algorithms: ["2"],
device_id: "dev2",
keys: { "ed25519:dev2": ed25519key },
signatures: {
chaz: {
"ed25519:dev2":
"8eaeXUWy9AQzjaNVOjVLs4FQk+cgobkNS811EjZBCMA" +
"apd8aPOfE26E13nFFOCLC1V6fOH5wVo61hxGR/j4PBA"
}
"FwslH/Q7EYSb7swDJbNB5PSzcbEO1xRRBF1riuijqvL" +
"EkrK9/XVN8jl4h7thGuRITQ01siBQnNmMK9t45QfcCQ",
},
},
unsigned: { "ghi": "def" },
}
};
var daveKeys = {
dev3: {
algorithms: ["3"], keys: { "ed25519:dev2": ed25519key },
signatures: {
dave: {
"ed25519:dev2":
"8eaeXUWy9AQzjaNVOjVLs4FQk+cgobkNS811EjZBCMA" +
"apd8aPOfE26E13nFFOCLC1V6fOH5wVo61hxGR/j4PBA"
}
},
unsigned: { "ghi": "def" },
}
user_id: "chaz",
},
};
/*
function sign(o) {
var anotherjson = require('another-json');
var b = JSON.parse(JSON.stringify(o));
delete(b.signatures);
delete(b.unsigned);
return client._crypto._olmDevice.sign(anotherjson.stringify(b));
};
console.log("Ed25519: " + ed25519key);
console.log("boris:", sign(borisKeys.dev1));
console.log("chaz:", sign(chazKeys.dev2));
*/
httpBackend.when("POST", "/keys/query").check(function(req) {
expect(req.data).toEqual({device_keys: {boris: {}, chaz: {}, dave: {}}});
expect(req.data).toEqual({device_keys: {boris: {}, chaz: {}}});
}).respond(200, {
device_keys: {
boris: borisKeys,
chaz: chazKeys,
dave: daveKeys,
},
});
client.downloadKeys(["boris", "chaz", "dave"]).then(function(res) {
client.downloadKeys(["boris", "chaz"]).then(function(res) {
assertObjectContains(res.boris.dev1, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev1": ed25519key },
@@ -245,29 +365,28 @@ describe("MatrixClient", function() {
assertObjectContains(res.chaz.dev2, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev2" : ed25519key },
keys: { "ed25519:dev2": ed25519key },
algorithms: ["2"],
unsigned: { "ghi": "def" },
});
// dave's key fails validation.
expect(res.dave).toEqual({});
}).catch(utils.failTest).done(done);
httpBackend.flush();
});
});
it("should return a rejected promise if the request fails", function(done) {
httpBackend.when("POST", "/keys/query").respond(400);
describe("deleteDevice", function() {
const auth = {a: 1};
it("should pass through an auth dict", function(done) {
httpBackend.when(
"DELETE", "/_matrix/client/unstable/devices/my_device",
).check(function(req) {
expect(req.data).toEqual({auth: auth});
}).respond(200);
var exceptionThrown;
client.downloadKeys(["bottom"]).then(function() {
fail("download didn't fail");
}, function(err) {
exceptionThrown = err;
}).then(function() {
expect(exceptionThrown).toBeTruthy();
}).catch(utils.failTest).done(done);
client.deleteDevice(
"my_device", auth,
).catch(utils.failTest).done(done);
httpBackend.flush();
});
@@ -275,7 +394,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]);
}
+43 -42
View File
@@ -1,17 +1,18 @@
"use strict";
var sdk = require("../..");
var MatrixClient = sdk.MatrixClient;
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
const sdk = require("../..");
const MatrixClient = sdk.MatrixClient;
const HttpBackend = require("../mock-request");
const utils = require("../test-utils");
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 +21,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 +70,7 @@ describe("MatrixClient opts", function() {
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
scheduler: new sdk.MatrixScheduler()
scheduler: new sdk.MatrixScheduler(),
});
});
@@ -78,9 +79,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 +91,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()
-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, {});
@@ -112,7 +113,7 @@ describe("MatrixClient opts", function() {
return httpBackend.flush("/sync", 1);
}).done(function() {
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes
0, "Expected to see event types: " + expectedEventTypes,
);
done();
});
@@ -127,14 +128,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 +148,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 +172,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");
+19 -19
View File
@@ -1,20 +1,21 @@
"use strict";
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
var EventStatus = sdk.EventStatus;
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const utils = require("../test-utils");
const EventStatus = sdk.EventStatus;
describe("MatrixClient retrying", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var scheduler;
var userId = "@alice:localhost";
var accessToken = "aseukfgwef";
var roomId = "!room:here";
var room;
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 +50,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 +59,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 +78,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() {
+149 -126
View File
@@ -1,28 +1,29 @@
"use strict";
var sdk = require("../..");
var EventStatus = sdk.EventStatus;
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
const sdk = require("../..");
const EventStatus = sdk.EventStatus;
const HttpBackend = require("../mock-request");
const utils = require("../test-utils");
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 +31,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 +68,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 +82,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 +91,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({
@@ -130,12 +129,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 +144,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 +157,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 +189,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 +216,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 +231,7 @@ describe("MatrixClient room timelines", function() {
return {
chunk: sbEvents,
start: "pagin_start",
end: sbEndTok
end: sbEndTok,
};
});
});
@@ -236,8 +239,10 @@ 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() {
@@ -263,49 +268,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 +327,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,13 +358,15 @@ 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);
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
expect(room.oldState.paginationToken).toBeDefined();
client.scrollback(room, 1).done(function() {
@@ -373,17 +384,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);
@@ -396,10 +409,10 @@ describe("MatrixClient room timelines", 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 +420,24 @@ 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];
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,18 +446,20 @@ 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;
});
@@ -451,10 +468,10 @@ describe("MatrixClient room timelines", 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]);
@@ -470,32 +487,34 @@ 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() {
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,15 +524,17 @@ 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() {
@@ -522,11 +543,11 @@ describe("MatrixClient room timelines", function() {
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,17 +556,19 @@ 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++;
+153 -153
View File
@@ -1,30 +1,31 @@
"use strict";
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
var MatrixEvent = sdk.MatrixEvent;
var EventTimeline = sdk.EventTimeline;
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const utils = require("../test-utils");
const MatrixEvent = sdk.MatrixEvent;
const EventTimeline = sdk.EventTimeline;
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 +37,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 +69,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,53 +87,53 @@ 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)
member.getAvatarUrl("home.server.url", null, null, null, false),
).toBeDefined();
done();
});
@@ -142,23 +142,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 +167,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 +186,7 @@ describe("MatrixClient syncing", function() {
});
client.startClient({
resolveInvitesToProfiles: true
resolveInvitesToProfiles: true,
});
httpBackend.flush().done(function() {
@@ -198,8 +198,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,10 +207,10 @@ 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)
member.getAvatarUrl("home.server.url", null, null, null, false),
).toBeNull();
done();
});
@@ -218,18 +218,18 @@ describe("MatrixClient syncing", function() {
});
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 +247,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 +328,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 +359,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 +375,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 +389,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,8 +403,8 @@ describe("MatrixClient syncing", function() {
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomTwo);
var member = room.getMember(otherUserId);
const room = client.getRoom(roomTwo);
let member = room.getMember(otherUserId);
expect(member).toBeDefined();
expect(member.typing).toEqual(true);
member = room.getMember(selfUserId);
@@ -425,7 +425,7 @@ describe("MatrixClient syncing", function() {
describe("timeline", function() {
beforeEach(function() {
var syncData = {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
@@ -435,7 +435,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 +449,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 +459,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 +469,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 +478,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 +488,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 +497,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 +519,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 +647,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 +658,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 +666,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");
File diff suppressed because it is too large Load Diff
+103 -44
View File
@@ -1,5 +1,5 @@
"use strict";
var q = require("q");
const q = require("q");
/**
* Construct a mock HTTP backend, heavily inspired by Angular.js.
@@ -8,26 +8,25 @@ 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 realReq = new Request(opts.method, opts.uri, opts.body, opts.qs);
realReq.callback = callback;
console.log("HTTP backend received request: %s %s", opts.method, opts.uri);
self.requests.push(realReq);
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(realReq);
const abort = function() {
const idx = self.requests.indexOf(req);
if (idx >= 0) {
console.log("Aborting HTTP request: %s %s", opts.method,
opts.uri);
self.requests.splice(idx, 1);
realReq.callback("aborted");
req.callback("aborted");
}
};
return {
abort: abort
abort: abort,
};
};
}
@@ -39,18 +38,18 @@ HttpBackend.prototype = {
* @return {Promise} resolved when there is nothing left to flush.
*/
flush: function(path, numToFlush) {
var defer = q.defer();
var self = this;
var flushed = 0;
var triedWaiting = false;
const defer = q.defer();
const self = this;
let flushed = 0;
let triedWaiting = false;
console.log(
"HTTP backend flushing... (path=%s numToFlush=%s)", path, numToFlush
"HTTP backend flushing... (path=%s numToFlush=%s)", path, numToFlush,
);
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
self.requests.length, self.expectedRequests.length, path,
);
if (self._takeFromQueue(path)) {
// try again on the next tick.
@@ -59,18 +58,15 @@ HttpBackend.prototype = {
if (numToFlush && flushed === numToFlush) {
console.log(" [%s] Flushed assigned amount: %s", path, numToFlush);
defer.resolve();
}
else {
} else {
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);
triedWaiting = true;
}
else {
} else {
console.log(" no more flushes. [%s]", path);
defer.resolve();
}
@@ -87,14 +83,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) ===
@@ -116,12 +117,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;
}
@@ -136,10 +137,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,
);
},
@@ -147,9 +148,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,
);
},
@@ -161,22 +162,32 @@ HttpBackend.prototype = {
* @return {Request} An expected request.
*/
when: function(method, path, data) {
var pendingReq = new Request(method, path, data);
const pendingReq = new ExpectedRequest(method, path, data);
this.expectedRequests.push(pendingReq);
return pendingReq;
}
},
};
function Request(method, path, data, queryParams) {
/**
* Represents the expectation of a request.
*
* <p>Includes the conditions to be matched against, the checks to be made,
* and the response to be returned.
*
* @constructor
* @param {string} method
* @param {string} path
* @param {object?} data
*/
function ExpectedRequest(method, path, data) {
this.method = method;
this.path = path;
this.data = data;
this.queryParams = queryParams;
this.callback = null;
this.response = null;
this.checks = [];
}
Request.prototype = {
ExpectedRequest.prototype = {
/**
* Execute a check when this request has been satisfied.
* @param {Function} fn The function to execute.
@@ -197,10 +208,10 @@ Request.prototype = {
this.response = {
response: {
statusCode: code,
headers: {}
headers: {},
},
body: data,
err: null
err: null,
};
},
@@ -213,12 +224,60 @@ Request.prototype = {
this.response = {
response: {
statusCode: code,
headers: {}
headers: {},
},
body: null,
err: err
err: err,
};
}
},
};
/**
* Represents a request made by the app.
*
* @constructor
* @param {object} opts opts passed to request()
* @param {function} callback
*/
function Request(opts, callback) {
this.opts = opts;
this.callback = callback;
Object.defineProperty(this, 'method', {
get: function() {
return opts.method;
},
});
Object.defineProperty(this, 'path', {
get: function() {
return opts.uri;
},
});
Object.defineProperty(this, 'data', {
get: function() {
return opts.body;
},
});
Object.defineProperty(this, 'queryParams', {
get: function() {
return opts.qs;
},
});
Object.defineProperty(this, 'headers', {
get: function() {
return opts.headers || {};
},
});
}
Request.prototype = {
toString: function() {
return this.method + " " + this.path;
},
};
/**
+61 -23
View File
@@ -1,6 +1,6 @@
"use strict";
var sdk = require("..");
var MatrixEvent = sdk.MatrixEvent;
const sdk = require("..");
const MatrixEvent = sdk.MatrixEvent;
/**
* Perform common actions before each test case, e.g. printing the test case
@@ -8,7 +8,7 @@ var MatrixEvent = sdk.MatrixEvent;
* @param {TestCase} testCase The test case that is about to be run.
*/
module.exports.beforeEach = function(testCase) {
var desc = testCase.suite.description + " : " + testCase.description;
const desc = testCase.suite.description + " : " + testCase.description;
console.log(desc);
console.log(new Array(1 + desc.length).join("="));
};
@@ -22,19 +22,18 @@ module.exports.beforeEach = function(testCase) {
module.exports.mock = function(constr, name) {
// By Tim Buschtöns
// 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();
const result = new HelperConstr();
result.jasmineToString = 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);
}
}
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 +57,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) {
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 +83,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 +91,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 +119,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 +149,7 @@ module.exports.mkMessage = function(opts) {
}
opts.content = {
msgtype: "m.text",
body: opts.msg
body: opts.msg,
};
return module.exports.mkEvent(opts);
};
@@ -159,7 +161,16 @@ module.exports.mkMessage = function(opts) {
* <p>This is useful for use with integration tests which use asyncronous
* methods: it can be added as a 'catch' handler in a promise chain.
*
* @param {Error} error exception to be reported
* @param {Error} err exception to be reported
*
* @deprecated
* It turns out there are easier ways of doing this. Just use nodeify():
*
* it("should not throw", function(done) {
* asynchronousMethod().then(function() {
* // some tests
* }).nodeify(done);
* });
*
* @example
* it("should not throw", function(done) {
@@ -168,6 +179,33 @@ module.exports.mkMessage = function(opts) {
* }).catch(utils.failTest).done(done);
* });
*/
module.exports.failTest = function(error) {
expect(error.stack).toBe(null);
module.exports.failTest = function(err) {
expect(true).toBe(false, "Testfunc threw: " + err.stack);
};
/**
* A mock implementation of webstorage
*
* @constructor
*/
module.exports.MockStorageApi = function() {
this.data = {};
};
module.exports.MockStorageApi.prototype = {
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;
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
},
};
+19 -19
View File
@@ -1,34 +1,34 @@
"use strict";
var ContentRepo = require("../../lib/content-repo");
var testUtils = require("../test-utils");
const ContentRepo = require("../../lib/content-repo");
const testUtils = require("../test-utils");
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 +38,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 +71,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",
);
});
});
+3 -3
View File
@@ -1,7 +1,7 @@
"use strict";
var Crypto = require("../../lib/crypto");
var sdk = require("../..");
const Crypto = require("../../lib/crypto");
const sdk = require("../..");
describe("Crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
@@ -9,6 +9,6 @@ describe("Crypto", function() {
}
it("Crypto exposes the correct olm library version", function() {
expect(Crypto.getOlmVersion()).toEqual([1, 3, 0]);
expect(Crypto.getOlmVersion()[0]).toEqual(2);
});
});
+62 -56
View File
@@ -1,7 +1,7 @@
"use strict";
var sdk = require("../..");
var EventTimeline = sdk.EventTimeline;
var utils = require("../test-utils");
const sdk = require("../..");
const EventTimeline = sdk.EventTimeline;
const utils = require("../test-utils");
function mockRoomStates(timeline) {
timeline._startState = utils.mock(sdk.RoomState, "startState");
@@ -9,24 +9,26 @@ function mockRoomStates(timeline) {
}
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 +39,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 +48,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);
}).not.toThrow();
timeline.addEvent(event, false);
expect(function() { timeline.initialiseState(state); }).toThrow();
expect(function() {
timeline.initialiseState(state);
}).toThrow();
});
});
@@ -99,8 +105,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,8 +114,8 @@ 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();
@@ -135,7 +141,7 @@ describe("EventTimeline", function() {
mockRoomStates(timeline);
});
var events = [
const events = [
utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry",
event: true,
@@ -148,7 +154,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 +164,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,15 +173,15 @@ 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) {
@@ -192,13 +198,13 @@ describe("EventTimeline", function() {
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,15 +215,15 @@ 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) {
@@ -234,11 +240,11 @@ describe("EventTimeline", function() {
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 +254,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);
@@ -278,16 +284,16 @@ describe("EventTimeline", function() {
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);
@@ -307,7 +313,7 @@ describe("EventTimeline", function() {
});
describe("removeEvent", function() {
var events = [
const events = [
utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry",
event: true,
@@ -327,7 +333,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 +365,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);
+15 -15
View File
@@ -1,24 +1,24 @@
"use strict";
var sdk = require("../..");
var Filter = sdk.Filter;
var utils = require("../test-utils");
const sdk = require("../..");
const Filter = sdk.Filter;
const utils = require("../test-utils");
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 +31,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);
+141
View File
@@ -0,0 +1,141 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
const q = require("q");
const sdk = require("../..");
const utils = require("../test-utils");
const InteractiveAuth = sdk.InteractiveAuth;
const MatrixError = sdk.MatrixError;
describe("InteractiveAuth", function() {
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
});
it("should start an auth stage and complete it", function(done) {
const doRequest = jasmine.createSpy('doRequest');
const startAuthStage = jasmine.createSpy('startAuthStage');
const ia = new InteractiveAuth({
doRequest: doRequest,
startAuthStage: startAuthStage,
authData: {
session: "sessionId",
flows: [
{ stages: ["logintype"] },
],
params: {
"logintype": { param: "aa" },
},
},
});
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams("logintype")).toEqual({
param: "aa",
});
// first we expect a call here
startAuthStage.andCallFake(function(stage) {
expect(stage).toEqual("logintype");
ia.submitAuthDict({
type: "logintype",
foo: "bar",
});
});
// .. which should trigger a call here
const requestRes = {"a": "b"};
doRequest.andCallFake(function(authData) {
expect(authData).toEqual({
session: "sessionId",
type: "logintype",
foo: "bar",
});
return q(requestRes);
});
ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest.calls.length).toEqual(1);
expect(startAuthStage.calls.length).toEqual(1);
}).catch(utils.failTest).done(done);
});
it("should make a request if no authdata is provided", function(done) {
const doRequest = jasmine.createSpy('doRequest');
const startAuthStage = jasmine.createSpy('startAuthStage');
const ia = new InteractiveAuth({
doRequest: doRequest,
startAuthStage: startAuthStage,
});
expect(ia.getSessionId()).toBe(undefined);
expect(ia.getStageParams("logintype")).toBe(undefined);
// first we expect a call to doRequest
doRequest.andCallFake(function(authData) {
console.log("request1", authData);
expect(authData).toBe(null);
const err = new MatrixError({
session: "sessionId",
flows: [
{ stages: ["logintype"] },
],
params: {
"logintype": { param: "aa" },
},
});
err.httpStatus = 401;
throw err;
});
// .. which should be followed by a call to startAuthStage
const requestRes = {"a": "b"};
startAuthStage.andCallFake(function(stage) {
expect(stage).toEqual("logintype");
expect(ia.getSessionId()).toEqual("sessionId");
expect(ia.getStageParams("logintype")).toEqual({
param: "aa",
});
// submitAuthDict should trigger another call to doRequest
doRequest.andCallFake(function(authData) {
console.log("request2", authData);
expect(authData).toEqual({
session: "sessionId",
type: "logintype",
foo: "bar",
});
return q(requestRes);
});
ia.submitAuthDict({
type: "logintype",
foo: "bar",
});
});
ia.attemptAuth().then(function(res) {
expect(res).toBe(requestRes);
expect(doRequest.calls.length).toEqual(2);
expect(startAuthStage.calls.length).toEqual(1);
}).catch(utils.failTest).done(done);
});
});
+126 -74
View File
@@ -1,44 +1,46 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var MatrixClient = sdk.MatrixClient;
var utils = require("../test-utils");
const q = require("q");
const sdk = require("../..");
const MatrixClient = sdk.MatrixClient;
const utils = require("../test-utils");
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;
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,13 +53,14 @@ describe("MatrixClient", function() {
// }
// items are popped off when processed and block if no items left.
];
var pendingLookup = null;
let acceptKeepalives;
let pendingLookup = null;
function httpReq(cb, method, path, qp, data, prefix) {
if (path === KEEP_ALIVE_PATH) {
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)
);
@@ -72,20 +75,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);
@@ -103,9 +106,10 @@ describe("MatrixClient", function() {
if (next.error) {
return q.reject({
errcode: next.error.errcode,
httpStatus: next.error.httpStatus,
name: next.error.errcode,
message: "Expected testing error",
data: next.error
data: next.error,
});
}
return q(next.data);
@@ -115,16 +119,16 @@ describe("MatrixClient", function() {
}
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
jasmine.Clock.useMock();
scheduler = jasmine.createSpyObj("scheduler", [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction"
"setProcessFunction",
]);
store = jasmine.createSpyObj("store", [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"setSyncToken", "storeEvents", "storeRoom", "storeUser",
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter"
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter",
]);
client = new MatrixClient({
baseUrl: "https://my.home.server",
@@ -133,18 +137,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", [
"authedRequest", "authedRequestWithPrefix", "getContentUri",
"request", "requestWithPrefix", "uploadContent"
"request", "requestWithPrefix", "uploadContent",
]);
client._http.authedRequest.andCallFake(httpReq);
client._http.authedRequestWithPrefix.andCallFake(httpReq);
client._http.requestWithPrefix.andCallFake(httpReq);
client._http.request.andCallFake(httpReq);
// set reasonable working defaults
acceptKeepalives = true;
pendingLookup = null;
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
@@ -170,9 +176,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();
@@ -187,7 +193,6 @@ describe("MatrixClient", function() {
});
describe("getSyncState", function() {
it("should return null if the client isn't started", function() {
expect(client.getSyncState()).toBeNull();
});
@@ -204,6 +209,44 @@ describe("MatrixClient", function() {
});
});
describe("getOrCreateFilter", function() {
it("should POST createFilter if no id is present in localStorage", function() {
});
it("should use an existing filter if id is present in localStorage", function() {
});
it("should handle localStorage filterId missing from the server", function(done) {
function getFilterName(userId, suffix) {
// scope this on the user ID because people may login on many accounts
// and they all need to be stored!
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
}
const invalidFilterId = 'invalidF1lt3r';
httpLookups = [];
httpLookups.push({
method: "GET",
path: FILTER_PATH + '/' + invalidFilterId,
error: {
errcode: "M_UNKNOWN",
name: "M_UNKNOWN",
message: "No row found",
data: { errcode: "M_UNKNOWN", error: "No row found" },
httpStatus: 404,
},
});
httpLookups.push(FILTER_RESPONSE);
store.getFilterIdByName.andReturn(invalidFilterId);
const filterName = getFilterName(client.credentials.userId);
client.store.setFilterIdByName(filterName, invalidFilterId);
const filter = new sdk.Filter(client.credentials.userId);
client.getOrCreateFilter(filterName, filter).then(function(filterId) {
expect(filterId).toEqual(FILTER_RESPONSE.data.filter_id);
done();
});
});
});
describe("retryImmediately", function() {
it("should return false if there is no request waiting", function() {
client.startClient();
@@ -214,7 +257,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);
@@ -237,19 +280,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);
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
jasmine.Clock.tick(10000);
} else if (state === "SYNCING" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
@@ -261,7 +306,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);
@@ -285,12 +330,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();
@@ -308,18 +352,18 @@ describe("MatrixClient", function() {
}
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));
@@ -328,29 +372,34 @@ describe("MatrixClient", function() {
it("should transition ERROR -> PREPARED after /sync if prev failed",
function(done) {
var expectedStates = [];
const expectedStates = [];
acceptKeepalives = false;
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
// We fail twice since the SDK ignores the first error.
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", error: { errcode: "NOPE_NOPE_NOPE2" }
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA
method: "GET", path: KEEP_ALIVE_PATH, data: {},
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA,
});
expectedStates.push(["ERROR", null]);
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["PREPARED", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
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));
@@ -358,27 +407,29 @@ describe("MatrixClient", function() {
});
it("should transition SYNCING -> ERROR after a failed /sync", function(done) {
var expectedStates = [];
// We fail twice since the SDK ignores the first error.
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: "/sync", error: { errcode: "NONONONONO2" }
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
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);
@@ -391,7 +442,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);
@@ -402,22 +453,25 @@ describe("MatrixClient", function() {
client.startClient();
});
it("should transition ERROR -> ERROR if multiple /sync fails", function(done) {
var expectedStates = [];
// We fail twice since the SDK ignores the first error.
it("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
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: "/sync", error: { errcode: "NONONONONO" }
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" }
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["ERROR", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
@@ -425,7 +479,7 @@ describe("MatrixClient", function() {
});
describe("inviteByEmail", function() {
var roomId = "!foo:bar";
const roomId = "!foo:bar";
it("should send an invite HTTP POST", function() {
httpLookups = [{
@@ -435,17 +489,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({
@@ -454,7 +506,7 @@ describe("MatrixClient", function() {
data: SYNC_DATA,
thenCall: function() {
done();
}
},
});
client.setGuest(true);
client.startClient();
+75 -76
View File
@@ -1,33 +1,32 @@
"use strict";
var PushProcessor = require("../../lib/pushprocessor");
var MatrixEvent = MatrixEvent;
var utils = require("../test-utils");
const PushProcessor = require("../../lib/pushprocessor");
const utils = require("../test-utils");
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 +37,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 +129,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 +203,8 @@ describe('NotificationService', function() {
event: true,
content: {
body: "",
msgtype: "m.text"
}
msgtype: "m.text",
},
});
pushProcessor = new PushProcessor(matrixClient);
});
@@ -214,25 +213,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 +239,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 +253,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 +280,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 +297,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);
});
});
+30 -29
View File
@@ -1,11 +1,11 @@
"use strict";
var callbacks = require("../../lib/realtime-callbacks");
var test_utils = require("../test-utils.js");
const callbacks = require("../../lib/realtime-callbacks");
const testUtils = require("../test-utils.js");
describe("realtime-callbacks", function() {
var clock = jasmine.Clock;
var fakeDate;
const clock = jasmine.Clock;
let fakeDate;
function tick(millis) {
// make sure we tick the fakedate first, otherwise nothing will happen!
@@ -14,10 +14,12 @@ describe("realtime-callbacks", function() {
}
beforeEach(function() {
test_utils.beforeEach(this);
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
clock.useMock();
fakeDate = Date.now();
callbacks.setNow(function() { return fakeDate; });
callbacks.setNow(function() {
return fakeDate;
});
});
afterEach(function() {
@@ -26,7 +28,7 @@ describe("realtime-callbacks", function() {
describe("setTimeout", function() {
it("should call the callback after the timeout", function() {
var callback = jasmine.createSpy();
const callback = jasmine.createSpy();
callbacks.setTimeout(callback, 100);
expect(callback).not.toHaveBeenCalled();
@@ -36,7 +38,7 @@ describe("realtime-callbacks", function() {
it("should default to a zero timeout", function() {
var callback = jasmine.createSpy();
const callback = jasmine.createSpy();
callbacks.setTimeout(callback);
expect(callback).not.toHaveBeenCalled();
@@ -45,17 +47,17 @@ describe("realtime-callbacks", function() {
});
it("should pass any parameters to the callback", function() {
var callback = jasmine.createSpy();
const callback = jasmine.createSpy();
callbacks.setTimeout(callback, 0, "a", "b", "c");
tick(0);
expect(callback).toHaveBeenCalledWith("a", "b", "c");
});
it("should set 'this' to the global object", function() {
var callback = jasmine.createSpy();
const callback = jasmine.createSpy();
callback.andCallFake(function() {
expect(this).toBe(global);
expect(this.console).toBeDefined();
expect(this).toBe(global); // eslint-disable-line no-invalid-this
expect(this.console).toBeDefined(); // eslint-disable-line no-invalid-this
});
callbacks.setTimeout(callback);
tick(0);
@@ -63,20 +65,20 @@ describe("realtime-callbacks", function() {
});
it("should handle timeouts of several seconds", function() {
var callback = jasmine.createSpy();
const callback = jasmine.createSpy();
callbacks.setTimeout(callback, 2000);
expect(callback).not.toHaveBeenCalled();
for (var i = 0; i < 4; i++) {
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 = jasmine.createSpy("callback1");
const callback2 = jasmine.createSpy("callback2");
const callback3 = jasmine.createSpy("callback3");
callbacks.setTimeout(callback2, 200);
callbacks.setTimeout(callback1, 100);
callbacks.setTimeout(callback3, 300);
@@ -99,8 +101,8 @@ 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 = jasmine.createSpy("callback1");
const callback2 = jasmine.createSpy("callback2");
// check that cb1 is called before cb2
callback1.andCallFake(function() {
@@ -118,8 +120,8 @@ describe("realtime-callbacks", function() {
});
it("should not get confused by chained calls", function() {
var callback2 = jasmine.createSpy("callback2");
var callback1 = jasmine.createSpy("callback1");
const callback2 = jasmine.createSpy("callback2");
const callback1 = jasmine.createSpy("callback1");
callback1.andCallFake(function() {
callbacks.setTimeout(callback2, 0);
expect(callback2).not.toHaveBeenCalled();
@@ -134,11 +136,11 @@ describe("realtime-callbacks", function() {
});
it("should be immune to exceptions", function() {
var callback1 = jasmine.createSpy("callback1");
const callback1 = jasmine.createSpy("callback1");
callback1.andCallFake(function() {
throw new Error("prepare to die");
});
var callback2 = jasmine.createSpy("callback2");
const callback2 = jasmine.createSpy("callback2");
callbacks.setTimeout(callback1, 0);
callbacks.setTimeout(callback2, 0);
@@ -148,24 +150,23 @@ describe("realtime-callbacks", function() {
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 = jasmine.createSpy();
const k = callbacks.setTimeout(callback);
callbacks.clearTimeout(k);
tick(0);
expect(callback).not.toHaveBeenCalled();
});
it("should not affect sooner timeouts", function() {
var callback1 = jasmine.createSpy("callback1");
var callback2 = jasmine.createSpy("callback2");
const callback1 = jasmine.createSpy("callback1");
const callback2 = jasmine.createSpy("callback2");
callbacks.setTimeout(callback1, 100);
var k = callbacks.setTimeout(callback2, 200);
const k = callbacks.setTimeout(callback2, 200);
callbacks.clearTimeout(k);
tick(100);
+54 -55
View File
@@ -1,22 +1,22 @@
"use strict";
var sdk = require("../..");
var RoomMember = sdk.RoomMember;
var utils = require("../test-utils");
const sdk = require("../..");
const RoomMember = sdk.RoomMember;
const utils = require("../test-utils");
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,10 +27,10 @@ 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);
@@ -38,20 +38,20 @@ describe("RoomMember", function() {
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 +59,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 +76,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 +84,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 +105,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 +113,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 +138,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 +165,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 +190,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,24 +218,26 @@ 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);
@@ -247,7 +249,7 @@ describe("RoomMember", function() {
});
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 +262,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 +273,5 @@ describe("RoomMember", function() {
member.setMembershipEvent(joinEvent); // no-op
expect(emitCount).toEqual(1);
});
});
});
+99 -99
View File
@@ -1,35 +1,35 @@
"use strict";
var sdk = require("../..");
var RoomState = sdk.RoomState;
var RoomMember = sdk.RoomMember;
var utils = require("../test-utils");
const sdk = require("../..");
const RoomState = sdk.RoomState;
const RoomMember = sdk.RoomMember;
const utils = require("../test-utils");
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,7 +40,7 @@ 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);
@@ -58,15 +58,15 @@ describe("RoomState", function() {
});
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 +81,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,7 +111,7 @@ 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);
@@ -120,24 +120,24 @@ describe("RoomState", function() {
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 +149,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 +168,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 +198,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]);
@@ -247,24 +247,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[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 +272,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 +284,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 +296,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 +316,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 +337,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 +367,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 +380,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 +404,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;
+256 -258
View File
File diff suppressed because it is too large Load Diff
+64 -60
View File
@@ -1,24 +1,27 @@
"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" */
const q = require("q");
const sdk = require("../..");
const MatrixScheduler = sdk.MatrixScheduler;
const MatrixError = sdk.MatrixError;
const utils = require("../test-utils");
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 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);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
jasmine.Clock.useMock();
scheduler = new MatrixScheduler(function(ev, attempts, err) {
if (retryFn) {
@@ -43,15 +46,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 +70,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;
}
@@ -109,25 +112,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 +149,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 +165,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();
@@ -197,7 +201,7 @@ describe("MatrixScheduler", function() {
queueFn = function() {
return "yep";
};
var prom = scheduler.queueEvent(eventA);
const prom = scheduler.queueEvent(eventA);
expect(prom).toBeDefined();
expect(prom.then).toBeDefined();
});
@@ -225,15 +229,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 +248,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 +284,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 +298,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 +312,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);
});
});
+98 -87
View File
@@ -1,28 +1,34 @@
"use strict";
var q = require("q");
var sdk = require("../..");
var EventTimeline = sdk.EventTimeline;
var TimelineWindow = sdk.TimelineWindow;
var TimelineIndex = require("../../lib/timeline-window").TimelineIndex;
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");
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 +41,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 +56,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 +66,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 +117,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 +134,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 +148,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 +162,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 +221,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 +234,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 +250,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 +265,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 +279,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 +295,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 +309,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 +324,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 +340,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 +354,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 +369,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 +385,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 +395,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 +418,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 +428,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 +451,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 +462,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))
+12 -12
View File
@@ -1,30 +1,30 @@
"use strict";
var sdk = require("../..");
var User = sdk.User;
var utils = require("../test-utils");
const sdk = require("../..");
const User = sdk.User;
const utils = require("../test-utils");
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 +35,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 +46,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;
});
+83 -55
View File
@@ -1,40 +1,40 @@
"use strict";
var utils = require("../../lib/utils");
var testUtils = require("../test-utils");
const utils = require("../../lib/utils");
const testUtils = require("../test-utils");
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 +44,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 +104,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 +125,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"]);
}).not.toThrow();
});
});
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"]);
}).not.toThrow();
});
});
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 +174,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 +216,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 +231,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));
}
+256 -136
View File
@@ -22,8 +22,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 +45,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 +60,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);
@@ -128,18 +133,30 @@ MatrixBaseApis.prototype.makeTxnId = function() {
MatrixBaseApis.prototype.register = function(
username, password,
sessionId, auth, bindEmail, guestAccessToken,
callback
callback,
) {
if (auth === undefined) { auth = {}; }
if (sessionId) { auth.session = sessionId; }
if (auth === undefined) {
auth = {};
}
if (sessionId) {
auth.session = sessionId;
}
var params = {
auth: auth
const params = {
auth: auth,
};
if (username !== undefined) { params.username = username; }
if (password !== undefined) { params.password = password; }
if (bindEmail !== undefined) { params.bind_email = bindEmail; }
if (guestAccessToken !== undefined) { params.guest_access_token = guestAccessToken; }
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 (guestAccessToken !== undefined && guestAccessToken !== null) {
params.guest_access_token = guestAccessToken;
}
return this.registerRequest(params, undefined, callback);
};
@@ -166,11 +183,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,
);
};
@@ -191,7 +210,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,
};
@@ -199,7 +218,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,
);
};
@@ -213,7 +232,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);
};
@@ -225,7 +244,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);
};
@@ -236,7 +255,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);
};
@@ -248,7 +267,7 @@ MatrixBaseApis.prototype.getCasLoginUrl = function(redirectUrl) {
*/
MatrixBaseApis.prototype.loginWithToken = function(token, callback) {
return this.login("m.login.token", {
token: token
token: token,
}, callback);
};
@@ -264,7 +283,7 @@ MatrixBaseApis.prototype.loginWithToken = function(token, callback) {
*/
MatrixBaseApis.prototype.logout = function(callback) {
return this._http.authedRequest(
callback, "POST", '/logout'
callback, "POST", '/logout',
);
};
@@ -279,17 +298,34 @@ 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,
);
};
/**
* Get the fallback URL to use for unknown interactive-auth stages.
*
* @param {string} loginType the type of stage being attempted
* @param {string} authSessionId the auth session ID provided by the homeserver
*
* @return {string} HS URL to hit to for the fallback interface
*/
MatrixBaseApis.prototype.getFallbackAuthUrl = function(loginType, authSessionId) {
const path = utils.encodeUri("/auth/$loginType/fallback/web", {
$loginType: loginType,
});
return this._http.getUrl(path, {
session: authSessionId,
}, httpApi.PREFIX_R0);
};
// Room operations
// ===============
@@ -311,7 +347,7 @@ MatrixBaseApis.prototype.deactivateAccount = function(auth, callback) {
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,
);
};
@@ -322,7 +358,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);
};
@@ -336,17 +372,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,
);
};
@@ -361,17 +397,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,
);
};
@@ -383,9 +419,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, {});
};
@@ -398,15 +434,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 },
);
};
@@ -415,6 +453,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.
@@ -435,7 +474,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;
@@ -445,7 +484,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,
);
}
};
@@ -459,14 +498,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,
);
};
@@ -479,11 +518,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,
);
};
@@ -496,11 +535,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,
);
};
@@ -512,7 +551,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);
};
@@ -525,8 +564,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);
};
@@ -543,11 +582,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 },
);
};
@@ -557,13 +620,38 @@ MatrixBaseApis.prototype.setRoomDirectoryVisibility =
/**
* Upload a file to the media repository on the home server.
* @param {File} file object
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
*
* @param {object} file The object to upload. On a browser, something that
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
* a a Buffer, String or ReadStream.
*
* @param {object} opts options object
*
* @param {string=} opts.name Name to give the file on the server. Defaults
* to <tt>file.name</tt>.
*
* @param {string=} opts.type Content-type for the upload. Defaults to
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
*
* @param {boolean=} opts.rawResponse Return the raw body, rather than
* parsing the JSON. Defaults to false (except on node.js, where it
* defaults to true for backwards compatibility).
*
* @param {boolean=} opts.onlyContentUri Just return the content URI,
* rather than the whole body. Defaults to false (except on browsers,
* where it defaults to true for backwards compatibility). Ignored if
* opts.rawResponse is true.
*
* @param {Function=} opts.callback Deprecated. Optional. The callback to
* invoke on success/failure. See the promise return values for more
* information.
*
* @return {module:client.Promise} Resolves to response object, as
* determined by this.opts.onlyData, opts.rawResponse, and
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
*/
MatrixBaseApis.prototype.uploadContent = function(file, callback) {
return this._http.uploadContent(file, callback);
MatrixBaseApis.prototype.uploadContent = function(file, opts) {
return this._http.uploadContent(file, opts);
};
/**
@@ -600,9 +688,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",
@@ -620,9 +710,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,
);
};
@@ -634,13 +724,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,
);
};
@@ -653,14 +762,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,
);
};
@@ -674,10 +783,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,
);
};
@@ -690,14 +799,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,
);
};
@@ -705,17 +814,24 @@ MatrixBaseApis.prototype.setDeviceDetails = function(device_id, body) {
* Delete the given device
*
* @param {string} device_id device to delete
* @param {object} auth Optional. Auth data to supply for User-Interactive auth.
* @return {module:client.Promise} Resolves: result object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.deleteDevice = function(device_id) {
var path = utils.encodeUri("/devices/$device_id", {
MatrixBaseApis.prototype.deleteDevice = function(device_id, auth) {
const path = utils.encodeUri("/devices/$device_id", {
$device_id: device_id,
});
const body = {};
if (auth) {
body.auth = auth;
}
return this._http.authedRequestWithPrefix(
undefined, "DELETE", path, undefined, undefined,
httpApi.PREFIX_UNSTABLE
undefined, "DELETE", path, undefined, body,
httpApi.PREFIX_UNSTABLE,
);
};
@@ -731,9 +847,9 @@ MatrixBaseApis.prototype.deleteDevice = function(device_id) {
* @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,
);
};
@@ -746,9 +862,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,
);
};
@@ -772,12 +888,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,
);
};
@@ -791,9 +907,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);
};
@@ -810,12 +926,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},
);
};
@@ -831,12 +947,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},
);
};
@@ -854,12 +970,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,
);
};
@@ -883,8 +999,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,
@@ -893,7 +1009,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,
);
};
@@ -908,42 +1024,46 @@ MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) {
* an error response ({@link module:http-api.MatrixError}).
*/
MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, callback) {
var downloadQuery = {};
const downloadQuery = {};
for (var i = 0; i < userIds.length; ++i) {
for (let i = 0; i < userIds.length; ++i) {
downloadQuery[userIds[i]] = {};
}
var content = {device_keys: downloadQuery};
const content = {device_keys: downloadQuery};
return this._http.authedRequestWithPrefix(
callback, "POST", "/keys/query", undefined, content,
httpApi.PREFIX_UNSTABLE
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 {module:client.callback=} callback
* @param {string} [key_algorithm = signed_curve25519] desired key type
*
* @return {module:client.Promise} Resolves: result object. Rejects: with
* an error response ({@link module:http-api.MatrixError}).
*/
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, callback) {
var queries = {};
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
const queries = {};
for (var i = 0; i < devices.length; ++i) {
var userId = devices[i][0];
var deviceId = devices[i][1];
var query = queries[userId] || {};
queries[userId] = query;
query[deviceId] = "curve25519";
if (key_algorithm === undefined) {
key_algorithm = "signed_curve25519";
}
var content = {one_time_keys: queries};
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;
}
const content = {one_time_keys: queries};
return this._http.authedRequestWithPrefix(
callback, "POST", "/keys/claim", undefined, content,
httpApi.PREFIX_UNSTABLE
undefined, "POST", "/keys/claim", undefined, content,
httpApi.PREFIX_UNSTABLE,
);
};
@@ -973,15 +1093,15 @@ MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, callback) {
*/
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,
);
};
@@ -997,13 +1117,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,
);
};
@@ -1022,20 +1142,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,
);
};
@@ -1050,7 +1170,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,
);
};
@@ -1063,13 +1183,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)));
}
},
};
+446
View File
@@ -0,0 +1,446 @@
/*
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';
import utils from '../utils';
/**
* @alias module:crypto/DeviceList
*/
export default class DeviceList {
constructor(baseApis, sessionStore, olmDevice) {
this._baseApis = baseApis;
this._sessionStore = sessionStore;
this._olmDevice = olmDevice;
// userId -> true
this._pendingUsersWithNewDevices = {};
// userId -> [promise, ...]
this._keyDownloadsInProgressByUser = {};
}
/**
* 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) {
const self = this;
// promises we need to wait for while the download happens
const promises = [];
// list of userids we need to download keys for
let downloadUsers = [];
function perUserCatch(u) {
return function(e) {
console.warn('Error downloading keys for user ' + u + ':', e);
};
}
if (forceDownload) {
downloadUsers = userIds;
} else {
for (let i = 0; i < userIds.length; ++i) {
const u = userIds[i];
const inprogress = this._keyDownloadsInProgressByUser[u];
if (inprogress) {
// wait for the download to complete
promises.push(q.any(inprogress).catch(perUserCatch(u)));
} else if (!this.getStoredDevicesForUser(u)) {
downloadUsers.push(u);
}
}
}
if (downloadUsers.length > 0) {
const r = this._doKeyDownloadForUsers(downloadUsers);
downloadUsers.map(function(u) {
promises.push(r[u].catch(perUserCatch(u)));
});
}
return q.all(promises).then(function() {
return self._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} sender_key curve25519 key to match
*
* @return {module:crypto/deviceinfo?}
*/
getDeviceByIdentityKey(userId, algorithm, sender_key) {
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 == sender_key) {
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 flushDeviceListRequests() for that.
*
* @param {String} userId
*/
invalidateUserDeviceList(userId) {
this._pendingUsersWithNewDevices[userId] = true;
}
/**
* Start device queries for any users who sent us an m.new_device recently
*/
flushNewDeviceRequests() {
const users = Object.keys(this._pendingUsersWithNewDevices);
if (users.length === 0) {
return;
}
const r = this._doKeyDownloadForUsers(users);
// we've kicked off requests to these users: remove their
// pending flag for now.
this._pendingUsersWithNewDevices = {};
users.map((u) => {
r[u] = r[u].catch((e) => {
console.error(
'Error updating device keys for user ' + u + ':', e,
);
// reinstate the pending flags on any users which failed; this will
// mean that we will do another download in the future, but won't
// tight-loop.
//
this._pendingUsersWithNewDevices[u] = true;
});
});
q.all(Object.values(r)).done();
}
/**
* @param {string[]} downloadUsers list of userIds
*
* @return {Object} a map from userId to a promise for a result for that user
*/
_doKeyDownloadForUsers(downloadUsers) {
const self = this;
console.log('Starting key download for ' + downloadUsers);
const deferMap = {};
const promiseMap = {};
downloadUsers.map(function(u) {
const deferred = q.defer();
const promise = deferred.promise.finally(function() {
const inProgress = self._keyDownloadsInProgressByUser[u];
utils.removeElement(inProgress, function(e) {
return e === promise;
});
if (inProgress.length === 0) {
// no more downloads for this user; remove the element
delete self._keyDownloadsInProgressByUser[u];
}
});
if (!self._keyDownloadsInProgressByUser[u]) {
self._keyDownloadsInProgressByUser[u] = [];
}
self._keyDownloadsInProgressByUser[u].push(promise);
deferMap[u] = deferred;
promiseMap[u] = promise;
});
this._baseApis.downloadKeysForUsers(
downloadUsers,
).done(function(res) {
const dk = res.device_keys || {};
for (let i = 0; i < downloadUsers.length; ++i) {
const userId = downloadUsers[i];
var deviceId;
console.log('got keys for ' + userId + ':', dk[userId]);
if (!dk[userId]) {
// no result for this user
const err = 'Unknown';
// TODO: do something with res.failures
deferMap[userId].reject(err);
continue;
}
// map from deviceid -> deviceinfo for this user
const userStore = {};
const devs = self._sessionStore.getEndToEndDevicesForUser(userId);
if (devs) {
for (deviceId in devs) {
if (devs.hasOwnProperty(deviceId)) {
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
userStore[deviceId] = d;
}
}
}
_updateStoredDeviceKeysForUser(
self._olmDevice, userId, userStore, dk[userId],
);
// update the session store
const storage = {};
for (deviceId in userStore) {
if (!userStore.hasOwnProperty(deviceId)) {
continue;
}
storage[deviceId] = userStore[deviceId].toStorage();
}
self._sessionStore.storeEndToEndDevicesForUser(
userId, storage,
);
deferMap[userId].resolve();
}
}, function(err) {
downloadUsers.map(function(u) {
deferMap[u].reject(err);
});
});
return promiseMap;
}
}
function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
userResult) {
let updated = false;
// remove any devices in the store which aren't in the response
for (var 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 (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.log("Device " + userId + ":" + deviceId +
" has no ed25519 key");
return false;
}
const unsigned = deviceResult.unsigned || {};
try {
olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
} catch (e) {
console.log("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());
@@ -58,17 +98,29 @@ function OlmDevice(sessionStore) {
// we don't bother stashing outboundgroupsessions in the sessionstore -
// instead we keep them here.
this._outboundGroupSessionStore = {};
// Store a set of decrypted message indexes for each group session.
// This partially mitigates a replay attack where a MITM resends a group
// message into the room.
//
// TODO: If we ever remove an event from memory we will also need to remove
// it from this map. Otherwise if we download the event from the server we
// will think that it is a duplicate.
//
// Keys are strings of form "<senderKey>|<session_id>|<message_index>"
// Values are true.
this._inboundGroupSessionMessageIndexes = {};
}
function _initialise_account(sessionStore, pickleKey, account) {
var e2eAccount = sessionStore.getEndToEndAccount();
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);
}
@@ -88,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 {
@@ -106,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);
};
@@ -121,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);
@@ -142,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,
);
};
@@ -157,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 {
@@ -207,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);
@@ -220,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);
@@ -237,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);
@@ -267,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);
@@ -304,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);
};
@@ -318,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;
}
@@ -339,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);
}
@@ -370,12 +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;
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;
});
@@ -393,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;
@@ -418,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;
@@ -430,7 +484,6 @@ OlmDevice.prototype.matchesSession = function(
};
// Outbound group session
// ======================
@@ -441,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;
};
@@ -456,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);
@@ -477,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);
@@ -497,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;
});
@@ -538,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),
);
};
@@ -568,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) {
@@ -585,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 || {});
@@ -608,19 +663,79 @@ 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;
var session = new Olm.InboundGroupSession();
const self = this;
/* if we already have this session, consider updating it */
function updateSession(session) {
console.log("Update for megolm session " + senderKey + "/" + sessionId);
// for now we just ignore updates. TODO: implement something here
return true;
}
const r = this._getInboundGroupSession(
roomId, senderKey, sessionId, updateSession,
);
if (r !== null) {
return;
}
// new session.
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();
@@ -641,32 +756,83 @@ 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);
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.
const messageIndexKey = senderKey + "|" + sessionId + "|" + res.message_index;
if (messageIndexKey in self._inboundGroupSessionMessageIndexes) {
throw new Error(
"Duplicate message index, possible replay attack: " +
messageIndexKey,
);
}
self._inboundGroupSessionMessageIndexes[messageIndexKey] = true;
}
// the sender must have had the senderKey to persuade us to save the
// session.
var keysProved = {curve25519: senderKey};
const keysProved = {curve25519: senderKey};
self._saveInboundGroupSession(
roomId, senderKey, sessionId, session, keysClaimed
roomId, senderKey, sessionId, session, keysClaimed,
);
return {
result: res,
result: plaintext,
keysClaimed: keysClaimed,
keysProved: keysProved,
};
}
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
// =========
@@ -683,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
@@ -45,13 +45,16 @@ module.exports.DECRYPTION_CLASSES = {};
* @alias module:crypto/algorithms/base.EncryptionAlgorithm
*
* @param {object} params parameters
* @param {string} params.userId The UserID for the local user
* @param {string} params.deviceId The identifier for this device.
* @param {module:crypto} params.crypto crypto core
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
* @param {string} params.roomId The ID of the room we will be sending to
* @param {object} params.config The body of the m.room.encryption event
*/
var EncryptionAlgorithm = function(params) {
const EncryptionAlgorithm = function(params) {
this._userId = params.userId;
this._deviceId = params.deviceId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
@@ -82,18 +85,9 @@ module.exports.EncryptionAlgorithm = EncryptionAlgorithm;
* @param {string=} oldMembership previous membership
*/
EncryptionAlgorithm.prototype.onRoomMembership = function(
event, member, oldMembership
event, member, oldMembership,
) {};
/**
* Called when a new device announces itself in the room
*
* @param {string} userId owner of the device
* @param {string} deviceId deviceId of the device
*/
EncryptionAlgorithm.prototype.onNewDevice = function(userId, deviceId) {};
/**
* base type for decryption implementations
*
@@ -101,10 +95,17 @@ EncryptionAlgorithm.prototype.onNewDevice = function(userId, deviceId) {};
* @alias module:crypto/algorithms/base.DecryptionAlgorithm
*
* @param {object} params parameters
* @param {string} params.userId The UserID for the local user
* @param {module:crypto} params.crypto crypto core
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
* @param {string=} params.roomId The ID of the room we will be receiving
* from. Null for to-device events.
*/
var DecryptionAlgorithm = function(params) {
const DecryptionAlgorithm = function(params) {
this._userId = params.userId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._roomId = params.roomId;
};
/** */
module.exports.DecryptionAlgorithm = DecryptionAlgorithm;
@@ -129,12 +130,21 @@ 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
*
@@ -147,6 +157,23 @@ module.exports.DecryptionError = function(msg) {
};
utils.inherits(module.exports.DecryptionError, Error);
/**
* Exception thrown specifically when we want to warn the user to consider
* the security of their conversation before continuing
*
* @constructor
* @param {string} msg message describing the problem
* @param {Object} devices userId -> {deviceId -> object}
* set of unknown devices per user we're warning about
* @extends Error
*/
module.exports.UnknownDeviceError = function(msg, devices) {
this.name = "UnknownDeviceError";
this.message = msg;
this.devices = devices;
};
utils.inherits(module.exports.UnknownDeviceError, Error);
/**
* 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");
+657
View File
@@ -0,0 +1,657 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* Defines m.olm encryption/decryption
*
* @module crypto/algorithms/megolm
*/
const q = require("q");
const utils = require("../../utils");
const olmlib = require("../olmlib");
const base = require("./base");
/**
* @private
* @constructor
*
* @param {string} sessionId
*
* @property {string} sessionId
* @property {Number} useCount number of times this session has been used
* @property {Number} creationTime when the session was created (ms since the epoch)
*
* @property {object} sharedWithDevices
* devices with which we have shared the session key
* userId -> {deviceId -> msgindex}
*/
function OutboundSessionInfo(sessionId) {
this.sessionId = sessionId;
this.useCount = 0;
this.creationTime = new Date().getTime();
this.sharedWithDevices = {};
}
/**
* Check if it's time to rotate the session
*
* @param {Number} rotationPeriodMsgs
* @param {Number} rotationPeriodMs
* @return {Boolean}
*/
OutboundSessionInfo.prototype.needsRotation = function(
rotationPeriodMsgs, rotationPeriodMs,
) {
const sessionLifetime = new Date().getTime() - this.creationTime;
if (this.useCount >= rotationPeriodMsgs ||
sessionLifetime >= rotationPeriodMs
) {
console.log(
"Rotating megolm session after " + this.useCount +
" messages, " + sessionLifetime + "ms",
);
return true;
}
return false;
};
/**
* Determine if this session has been shared with devices which it shouldn't
* have been.
*
* @param {Object} devicesInRoom userId -> {deviceId -> object}
* devices we should shared the session with.
*
* @return {Boolean} true if we have shared the session with devices which aren't
* in devicesInRoom.
*/
OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
devicesInRoom,
) {
for (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
*
* @constructor
* @extends {module:crypto/algorithms/base.EncryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.EncryptionAlgorithm}
*/
function MegolmEncryption(params) {
base.EncryptionAlgorithm.call(this, params);
// the most recent attempt to set up a session. This is used to serialise
// the session setups, so that we have a race-free view of which session we
// are using, and which devices we have shared the keys with. It resolves
// with an OutboundSessionInfo (or undefined, for the first message in the
// room).
this._setupPromise = q();
// default rotation periods
this._sessionRotationPeriodMsgs = 100;
this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
if (params.config.rotation_period_ms !== undefined) {
this._sessionRotationPeriodMs = params.config.rotation_period_ms;
}
if (params.config.rotation_period_msgs !== undefined) {
this._sessionRotationPeriodMsgs = params.config.rotation_period_msgs;
}
}
utils.inherits(MegolmEncryption, base.EncryptionAlgorithm);
/**
* @private
*
* @param {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(devicesInRoom) {
const self = this;
let session;
// takes the previous OutboundSessionInfo, and considers whether to create
// a new one. Also shares the key with any (new) devices in the room.
// Updates `session` to hold the final OutboundSessionInfo.
//
// returns a promise which resolves once the keyshare is successful.
function prepareSession(oldSession) {
session = oldSession;
// need to make a brand new session?
if (session && session.needsRotation(self._sessionRotationPeriodMsgs,
self._sessionRotationPeriodMs)
) {
console.log("Starting new megolm session because we need to rotate.");
session = null;
}
// determine if we have shared with anyone we shouldn't have
if (session && session.sharedWithTooManyDevices(devicesInRoom)) {
session = null;
}
if (!session) {
session = self._prepareNewSession();
}
// now check if we need to share with any devices
const shareMap = {};
for (const userId in devicesInRoom) {
if (!devicesInRoom.hasOwnProperty(userId)) {
continue;
}
const userDevices = devicesInRoom[userId];
for (const deviceId in userDevices) {
if (!userDevices.hasOwnProperty(deviceId)) {
continue;
}
const deviceInfo = userDevices[deviceId];
const key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
continue;
}
if (
!session.sharedWithDevices[userId] ||
session.sharedWithDevices[userId][deviceId] === undefined
) {
shareMap[userId] = shareMap[userId] || [];
shareMap[userId].push(deviceInfo);
}
}
}
return self._shareKeyWithDevices(
session, shareMap,
);
}
// helper which returns the session prepared by prepareSession
function returnSession() {
return session;
}
// first wait for the previous share to complete
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
*
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*/
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},
);
return new OutboundSessionInfo(session_id);
};
/**
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @return {module:client.Promise} Promise which resolves once the key sharing
* message has been sent.
*/
MegolmEncryption.prototype._shareKeyWithDevices = function(session, devicesByUser) {
const self = this;
const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
const payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: session.sessionId,
session_key: key.key,
chain_index: key.chain_index,
},
};
const contentMap = {};
return olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser,
).then(function(devicemap) {
let haveTargets = false;
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
const devicesToShareWith = devicesByUser[userId];
const sessionResults = devicemap[userId];
for (let i = 0; i < devicesToShareWith.length; i++) {
const deviceInfo = devicesToShareWith[i];
const deviceId = deviceInfo.deviceId;
const sessionResult = sessionResults[deviceId];
if (!sessionResult.sessionId) {
// no session with this device, probably because there
// were no one-time keys.
//
// we could send them a to_device message anyway, as a
// signal that they have missed out on the key sharing
// message because of the lack of keys, but there's not
// much point in that really; it will mostly serve to clog
// up to_device inboxes.
//
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
continue;
}
console.log(
"sharing keys with device " + userId + ":" + deviceId,
);
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
self._userId,
self._deviceId,
self._olmDevice,
userId,
deviceInfo,
payload,
);
if (!contentMap[userId]) {
contentMap[userId] = {};
}
contentMap[userId][deviceId] = encryptedContent;
haveTargets = true;
}
}
if (!haveTargets) {
return q();
}
// TODO: retries
return self._baseApis.sendToDevice("m.room.encrypted", contentMap);
}).then(function() {
// Add the devices we have shared with to session.sharedWithDevices.
//
// we deliberately iterate over devicesByUser (ie, the devices we
// attempted to share with) rather than the contentMap (those we did
// share with), because we don't want to try to claim a one-time-key
// for dead devices on every message.
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
if (!session.sharedWithDevices[userId]) {
session.sharedWithDevices[userId] = {};
}
const devicesToShareWith = devicesByUser[userId];
for (let i = 0; i < devicesToShareWith.length; i++) {
const deviceInfo = devicesToShareWith[i];
session.sharedWithDevices[userId][deviceInfo.deviceId] =
key.chain_index;
}
}
});
};
/**
* @inheritdoc
*
* @param {module:models/room} room
* @param {string} eventType
* @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) {
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,
};
const ciphertext = self._olmDevice.encryptGroupMessage(
session.sessionId, JSON.stringify(payloadJson),
);
const encryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext,
session_id: session.sessionId,
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
device_id: self._deviceId,
};
session.useCount++;
return encryptedContent;
});
};
/**
* 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 {Object} devicesInRoom userId -> {deviceId -> object}
* devices we should shared the session with.
*/
MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) {
const unknownDevices = {};
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);
}
};
/**
* Get the list of unblocked devices for all users in the room
*
* @param {module:models/room} room
*
* @return {module:client.Promise} Promise which resolves to a map
* from userId to deviceId to deviceInfo
*/
MegolmEncryption.prototype._getDevicesInRoom = function(room) {
// XXX what about rooms where invitees can see the content?
const roomMembers = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
// We are happy to use a cached version here: we assume that if we already
// have a list of the user's devices, then we already share an e2e room
// with them, which means that they will have announced any new devices via
// an m.new_device.
//
// 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;
});
};
/**
* Megolm decryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.DecryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.DecryptionAlgorithm}
*/
function MegolmDecryption(params) {
base.DecryptionAlgorithm.call(this, params);
// events which we couldn't decrypt due to unknown sessions / indexes: map from
// senderKey|sessionId to list of MatrixEvents
this._pendingEvents = {};
}
utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
/**
* @inheritdoc
*
* @param {MatrixEvent} event
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
MegolmDecryption.prototype.decryptEvent = function(event) {
const content = event.getWireContent();
if (!content.sender_key || !content.session_id ||
!content.ciphertext
) {
throw new base.DecryptionError("Missing fields in input");
}
let res;
try {
res = this._olmDevice.decryptGroupMessage(
event.getRoomId(), content.sender_key, content.session_id, content.ciphertext,
);
} catch (e) {
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
this._addEventToPendingList(event);
}
throw new base.DecryptionError(e);
}
if (res === null) {
// We've got a message for a session we don't have.
this._addEventToPendingList(event);
throw new base.DecryptionError(
"The sender's device has not sent us the keys for this message.",
);
}
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,
);
}
event.setClearData(payload, res.keysProved, res.keysClaimed);
};
/**
* Add an event to the list of those we couldn't decrypt the first time we
* saw them.
*
* @private
*
* @param {module:models/event.MatrixEvent} event
*/
MegolmDecryption.prototype._addEventToPendingList = function(event) {
const content = event.getWireContent();
const k = content.sender_key + "|" + content.session_id;
if (!this._pendingEvents[k]) {
this._pendingEvents[k] = [];
}
this._pendingEvents[k].push(event);
};
/**
* @inheritdoc
*
* @param {module:models/event.MatrixEvent} event key event
*/
MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
console.log("Adding key from ", event);
const content = event.getContent();
if (!content.room_id ||
!content.session_id ||
!content.session_key
) {
console.error("key event is missing fields");
return;
}
this._olmDevice.addInboundGroupSession(
content.room_id, event.getSenderKey(), content.session_id,
content.session_key, event.getKeysClaimed(),
);
// have another go at decrypting events sent with this session.
this._retryDecryption(event.getSenderKey, content.session_id);
};
/**
* @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,
);
@@ -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,7 +63,7 @@ OlmEncryption.prototype._ensureSession = function(roomMembers) {
return q();
}
var self = this;
const self = this;
this._prepPromise = self._crypto.downloadKeys(roomMembers, true).then(function(res) {
return self._crypto.ensureOlmSessionsForUsers(roomMembers);
}).then(function() {
@@ -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,38 +89,49 @@ 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 participantKeys = [];
for (var i = 0; i < users.length; ++i) {
var userId = users[i];
var devices = self._crypto.getStoredDevicesForUser(userId);
for (var j = 0; j < devices.length; ++j) {
var deviceInfo = devices[j];
var key = deviceInfo.getIdentityKey();
const payloadFields = {
room_id: room.roomId,
type: eventType,
content: content,
};
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
for (let i = 0; i < users.length; ++i) {
const userId = users[i];
const devices = self._crypto.getStoredDevicesForUser(userId);
for (let j = 0; j < devices.length; ++j) {
const deviceInfo = devices[j];
const key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
// don't bother setting up session to ourself
// don't bother sending to ourself
continue;
}
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
// don't bother setting up sessions with blocked users
continue;
}
participantKeys.push(key);
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
self._userId, self._deviceId, self._olmDevice,
userId, deviceInfo, payloadFields,
);
}
}
return olmlib.encryptMessageForDevices(
self._deviceId, self._olmDevice, participantKeys, {
room_id: room.roomId,
type: eventType,
content: content,
}
);
return encryptedContent;
});
};
@@ -140,17 +151,15 @@ utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
/**
* @inheritdoc
*
* @param {object} event raw event
*
* @return {module:crypto.DecryptionResult} decryption result
* @param {MatrixEvent} event
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
OlmDecryption.prototype.decryptEvent = function(event) {
var content = event.content;
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");
@@ -159,30 +168,69 @@ 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.event_id + ") from " + deviceKey +
": " + e.message
event.getId() + ") from " + deviceKey +
": " + e.message,
);
throw new base.DecryptionError("Bad Encrypted Message");
}
const payload = JSON.parse(payloadString);
// TODO: Check the sender user id matches the sender key.
// TODO: check the room_id and fingerprint
var payload = JSON.parse(payloadString);
return {
payload: payload,
sessionExists: true,
keysProved: {curve25519: deviceKey},
keysClaimed: payload.keys || {}
};
// check that we were the intended recipient, to avoid unknown-key attack
// https://github.com/vector-im/vector-web/issues/2483
if (payload.recipient != this._userId) {
console.warn(
"Event " + event.getId() + ": Intended recipient " +
payload.recipient + " does not match our id " + this._userId,
);
throw new base.DecryptionError(
"Message was intented for " + payload.recipient,
);
}
if (payload.recipient_keys.ed25519 !=
this._olmDevice.deviceEd25519Key) {
console.warn(
"Event " + event.getId() + ": Intended recipient ed25519 key " +
payload.recipient_keys.ed25519 + " did not match ours",
);
throw new base.DecryptionError("Message not intended for this device");
}
// check that the original sender matches what the homeserver told us, to
// avoid people masquerading as others.
// (this check is also provided via the sender's embedded ed25519 key,
// which is checked elsewhere).
if (payload.sender != event.getSender()) {
console.warn(
"Event " + event.getId() + ": original sender " + payload.sender +
" does not match reported sender " + event.getSender(),
);
throw new base.DecryptionError(
"Message forwarded from " + payload.sender,
);
}
// Olm events intended for a room have a room_id.
if (payload.room_id !== event.getRoomId()) {
console.warn(
"Event " + event.getId() + ": original room " + payload.room_id +
" does not match reported room " + event.room_id,
);
throw new base.DecryptionError(
"Message intended for room " + payload.room_id,
);
}
event.setClearData(payload, {curve25519: deviceKey}, payload.keys || {});
};
@@ -195,24 +243,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) {
@@ -220,7 +268,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,
);
}
@@ -240,29 +288,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
+273
View File
@@ -0,0 +1,273 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module olmlib
*
* Utilities common to olm encryption algorithms
*/
const q = require('q');
const anotherjson = require('another-json');
const utils = require("../utils");
/**
* matrix algorithm tag for olm
*/
module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
/**
* matrix algorithm tag for megolm
*/
module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
/**
* Encrypt an event payload for an Olm device
*
* @param {Object<string, string>} resultsObject The `ciphertext` property
* of the m.room.encrypted event to which to add our result
*
* @param {string} ourUserId
* @param {string} ourDeviceId
* @param {module:crypto/OlmDevice} olmDevice olm.js wrapper
* @param {string} recipientUserId
* @param {module:crypto/deviceinfo} recipientDevice
* @param {object} payloadFields fields to include in the encrypted payload
*/
module.exports.encryptMessageForDevice = function(
resultsObject,
ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice,
payloadFields,
) {
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.
return;
}
console.log(
"Using sessionid " + sessionId + " for device " +
recipientUserId + ":" + recipientDevice.deviceId,
);
const payload = {
sender: ourUserId,
sender_device: ourDeviceId,
// Include the Ed25519 key so that the recipient knows what
// device this message came from.
// We don't need to include the curve25519 key since the
// recipient will already know this from the olm headers.
// When combined with the device keys retrieved from the
// homeserver signed by the ed25519 key this proves that
// the curve25519 key and the ed25519 key are owned by
// the same device.
keys: {
"ed25519": olmDevice.deviceEd25519Key,
},
// include the recipient device details in the payload,
// to avoid unknown key attacks, per
// https://github.com/vector-im/vector-web/issues/2483
recipient: recipientUserId,
recipient_keys: {
"ed25519": recipientDevice.getFingerprint(),
},
};
// TODO: technically, a bunch of that stuff only needs to be included for
// pre-key messages: after that, both sides know exactly which devices are
// involved in the session. If we're looking to reduce data transfer in the
// future, we could elide them for subsequent messages.
utils.extend(payload, payloadFields);
resultsObject[deviceKey] = olmDevice.encryptMessage(
deviceKey, sessionId, JSON.stringify(payload),
);
};
/**
* Try to make sure we have established olm sessions for the given devices.
*
* @param {module:crypto/OlmDevice} olmDevice
*
* @param {module:base-apis~MatrixBaseApis} baseApis
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @return {module:client.Promise} resolves once the sessions are complete, to
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
*/
module.exports.ensureOlmSessionsForDevices = function(
olmDevice, baseApis, devicesByUser,
) {
const devicesWithoutSession = [
// [userId, deviceId], ...
];
const result = {};
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
result[userId] = {};
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]);
}
result[userId][deviceId] = {
device: deviceInfo,
sessionId: sessionId,
};
}
}
if (devicesWithoutSession.length === 0) {
return q(result);
}
// TODO: this has a race condition - if we try to send another message
// while we are claiming a key, we will end up claiming two and setting up
// two sessions.
//
// That should eventually resolve itself, but it's poor form.
const oneTimeKeyAlgorithm = "signed_curve25519";
return baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm,
).then(function(res) {
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;
}
const deviceRes = userRes[deviceId] || {};
let oneTimeKey = null;
for (const keyId in deviceRes) {
if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) {
oneTimeKey = deviceRes[keyId];
}
}
if (!oneTimeKey) {
console.warn(
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
") for device " + userId + ":" + deviceId,
);
continue;
}
const sid = _verifyKeyAndStartSession(
olmDevice, oneTimeKey, userId, deviceInfo,
);
result[userId][deviceId].sessionId = sid;
}
}
return result;
});
};
function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
const deviceId = deviceInfo.deviceId;
try {
_verifySignature(
olmDevice, oneTimeKey, userId, deviceId,
deviceInfo.getFingerprint(),
);
} catch (e) {
console.error(
"Unable to verify signature on one-time key for device " +
userId + ":" + deviceId + ":", e,
);
return null;
}
let sid;
try {
sid = olmDevice.createOutboundSession(
deviceInfo.getIdentityKey(), oneTimeKey.key,
);
} catch (e) {
// possibly a bad key
console.error("Error starting session with device " +
userId + ":" + deviceId + ": " + e);
return null;
}
console.log("Started new sessionid " + sid +
" for device " + userId + ":" + deviceId);
return sid;
}
/**
* Verify the signature on an object
*
* @param {module:crypto/OlmDevice} olmDevice olm wrapper to use for verify op
*
* @param {Object} obj object to check signature on. Note that this will be
* stripped of its 'signatures' and 'unsigned' properties.
*
* @param {string} signingUserId ID of the user whose signature should be checked
*
* @param {string} signingDeviceId ID of the device whose signature should be checked
*
* @param {string} signingKey base64-ed ed25519 public key
*/
const _verifySignature = module.exports.verifySignature = function(
olmDevice, obj, signingUserId, signingDeviceId, signingKey,
) {
const signKeyId = "ed25519:" + signingDeviceId;
const signatures = obj.signatures || {};
const userSigs = signatures[signingUserId] || {};
const signature = userSigs[signKeyId];
if (!signature) {
throw Error("No signature");
}
// prepare the canonical json: remove unsigned and signatures, and stringify with
// anotherjson
delete obj.unsigned;
delete obj.signatures;
const json = anotherjson.stringify(obj);
olmDevice.verifySignature(
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;
};
+248 -105
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:
@@ -63,13 +63,17 @@ module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
* requests. This function must look like function(opts, callback){ ... }.
* @param {string} opts.prefix Required. The matrix client prefix to use, e.g.
* '/_matrix/client/r0'. See PREFIX_R0 and PREFIX_UNSTABLE for constants.
* @param {bool} opts.onlyData True to return only the 'data' component of the
* response (e.g. the parsed HTTP body). If false, requests will return status
* codes and headers in addition to data. Default: false.
*
* @param {bool=} opts.onlyData True to return only the 'data' component of the
* response (e.g. the parsed HTTP body). If false, requests will return an
* object with the properties <tt>code</tt>, <tt>headers</tt> and <tt>data</tt>.
*
* @param {string} opts.accessToken The access_token to send with requests. Can be
* null to not send an access token.
* @param {Object} opts.extraParams Optional. Extra query parameters to send on
* @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"]);
@@ -87,32 +91,99 @@ 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,
};
},
/**
* Upload content to the Home Server
* @param {File} file A File object (in a browser) or in Node,
an object with properties:
name: The file's name
stream: A read stream
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
*
* @param {object} file The object to upload. On a browser, something that
* can be sent to XMLHttpRequest.send (typically a File). Under node.js,
* a Buffer, String or ReadStream.
*
* @param {object} opts options object
*
* @param {string=} opts.name Name to give the file on the server. Defaults
* to <tt>file.name</tt>.
*
* @param {string=} opts.type Content-type for the upload. Defaults to
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
*
* @param {boolean=} opts.rawResponse Return the raw body, rather than
* parsing the JSON. Defaults to false (except on node.js, where it
* defaults to true for backwards compatibility).
*
* @param {boolean=} opts.onlyContentUri Just return the content URI,
* rather than the whole body. Defaults to false (except on browsers,
* where it defaults to true for backwards compatibility). Ignored if
* opts.rawResponse is true.
*
* @param {Function=} opts.callback Deprecated. Optional. The callback to
* invoke on success/failure. See the promise return values for more
* information.
*
* @return {module:client.Promise} Resolves to response object, as
* determined by this.opts.onlyData, opts.rawResponse, and
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
*/
uploadContent: function(file, callback) {
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
);
uploadContent: function(file, opts) {
if (utils.isFunction(opts)) {
// opts used to be callback
opts = {
callback: opts,
};
} else if (opts === undefined) {
opts = {};
}
// if the file doesn't have a mime type, use a default since
// the HS errors if we don't supply one.
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.
const body = file.stream ? file.stream : file;
// backwards-compatibility hacks where we used to do different things
// between browser and node.
let rawResponse = opts.rawResponse;
if (rawResponse === undefined) {
if (global.XMLHttpRequest) {
rawResponse = false;
} else {
console.warn(
"Returning the raw JSON from uploadContent(). Future " +
"versions of the js-sdk will change this default, to " +
"return the parsed object. Set opts.rawResponse=false " +
"to change this behaviour now.",
);
rawResponse = true;
}
}
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.",
);
onlyContentUri = true;
} else {
onlyContentUri = false;
}
}
// browser-request doesn't support File objects because it deep-copies
// the options using JSON.parse(JSON.stringify(options)). Instead of
// loading the whole file into memory as a string and letting
@@ -121,42 +192,61 @@ 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;
if (global.XMLHttpRequest) {
var defer = q.defer();
var xhr = new global.XMLHttpRequest();
upload.xhr = xhr;
var cb = requestCallback(defer, callback, this.opts.onlyData);
const upload = { loaded: 0, total: 0 };
let promise;
var timeout_fn = function() {
// 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.
let bodyParser = null;
if (!rawResponse) {
bodyParser = function(rawBody) {
let body = JSON.parse(rawBody);
if (onlyContentUri) {
body = body.content_uri;
if (body === undefined) {
throw Error('Bad response');
}
}
return body;
};
}
if (global.XMLHttpRequest) {
const defer = q.defer();
const xhr = new global.XMLHttpRequest();
upload.xhr = xhr;
const cb = requestCallback(defer, opts.callback, this.opts.onlyData);
const timeout_fn = function() {
xhr.abort();
cb(new Error('Timeout'));
};
// set an initial timeout of 30s; we'll advance it each time we get
// a progress notification
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
xhr.onreadystatechange = function() {
switch (xhr.readyState) {
case global.XMLHttpRequest.DONE:
callbacks.clearTimeout(xhr.timeout_timer);
var err;
if (!xhr.responseText) {
err = new Error('No response body.');
var resp;
try {
if (!xhr.responseText) {
throw new Error('No response body.');
}
resp = xhr.responseText;
if (bodyParser) {
resp = bodyParser(resp);
}
} catch (err) {
err.http_status = xhr.status;
cb(err);
return;
}
var resp = JSON.parse(xhr.responseText);
if (resp.content_uri === undefined) {
err = Error('Bad response');
err.http_status = xhr.status;
cb(err);
return;
}
cb(undefined, xhr, resp.content_uri);
cb(undefined, xhr, resp);
break;
}
};
@@ -167,41 +257,37 @@ 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(file.name);
url += "&filename=" + encodeURIComponent(fileName);
xhr.open("POST", url);
if (file.type) {
xhr.setRequestHeader("Content-Type", file.type);
} else {
// if the file doesn't have a mime type, use a default since
// the HS errors if we don't supply one.
xhr.setRequestHeader("Content-Type", 'application/octet-stream');
}
xhr.send(file);
xhr.setRequestHeader("Content-Type", contentType);
xhr.send(body);
promise = defer.promise;
// dirty hack (as per _request) to allow the upload to be cancelled.
promise.abort = xhr.abort.bind(xhr);
} else {
var queryParams = {
filename: file.name,
const queryParams = {
filename: fileName,
};
promise = this.authedRequest(
callback, "POST", "/upload", queryParams, file.stream, {
opts.callback, "POST", "/upload", queryParams, body, {
prefix: "/_matrix/media/v1",
localTimeoutMs: 30000,
headers: {"Content-Type": file.type},
}
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;
@@ -231,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;
@@ -252,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
@@ -303,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");
@@ -351,11 +437,11 @@ module.exports.MatrixHttpApi.prototype = {
*/
request: function(callback, method, path, queryParams, data, opts) {
opts = opts || {};
var prefix = 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,
);
},
@@ -390,7 +476,7 @@ module.exports.MatrixHttpApi.prototype = {
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
}
},
);
},
@@ -425,7 +511,7 @@ module.exports.MatrixHttpApi.prototype = {
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
}
},
);
},
@@ -465,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,
);
},
@@ -486,34 +572,65 @@ module.exports.MatrixHttpApi.prototype = {
* @return {string} URL
*/
getUrl: function(path, queryParams, prefix) {
var queryString = "";
let queryString = "";
if (queryParams) {
queryString = "?" + utils.encodeParams(queryParams);
}
return this.opts.baseUrl + prefix + path + queryString;
},
/**
* @private
*
* @param {function} callback
* @param {string} method
* @param {string} uri
* @param {object} queryParams
* @param {object|string} data
* @param {object=} opts
*
* @param {boolean} [opts.json =true] Json-encode data before sending, and
* decode response on receipt. (We will still json-decode error
* responses, even if this is false.)
*
* @param {object=} opts.headers extra request headers
*
* @param {number=} opts.localTimeoutMs client-side timeout for the
* request. Default timeout if falsy.
*
* @param {function=} opts.bodyParser function to parse the body of the
* response before passing it to the promise and callback.
*
* @return {module:client.Promise} a promise which resolves to either the
* response object (if this.opts.onlyData is truthy), or the parsed
* body. Rejects
*/
_request: function(callback, method, uri, queryParams, data, opts) {
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
"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 defer = q.defer();
var timeoutId;
var timedOut = false;
var req;
var localTimeoutMs = opts.localTimeoutMs;
const json = opts.json === undefined ? true : opts.json;
const defer = q.defer();
let timeoutId;
let timedOut = false;
let req;
const localTimeoutMs = opts.localTimeoutMs || this.opts.localTimeoutMs;
if (localTimeoutMs) {
timeoutId = callbacks.setTimeout(function() {
timedOut = true;
@@ -523,12 +640,12 @@ module.exports.MatrixHttpApi.prototype = {
defer.reject(new module.exports.MatrixError({
error: "Locally timed out waiting for a response",
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
timeout: localTimeoutMs
timeout: localTimeoutMs,
}));
}, localTimeoutMs);
}
var reqPromise = defer.promise;
const reqPromise = defer.promise;
try {
req = this.opts.request(
@@ -538,10 +655,10 @@ module.exports.MatrixHttpApi.prototype = {
withCredentials: false,
qs: queryParams,
body: data,
json: true,
json: json,
timeout: localTimeoutMs,
headers: opts.headers || {},
_matrix_opts: this.opts
_matrix_opts: this.opts,
},
function(err, response, body) {
if (localTimeoutMs) {
@@ -550,24 +667,31 @@ module.exports.MatrixHttpApi.prototype = {
return; // already rejected promise
}
}
var handlerFn = requestCallback(defer, callback, self.opts.onlyData);
// if json is falsy, we won't parse any error response, so need
// to do so before turning it into a MatrixError
const parseErrorJson = !json;
const handlerFn = requestCallback(
defer, callback, self.opts.onlyData,
parseErrorJson,
opts.bodyParser,
);
handlerFn(err, response, body);
}
},
);
if (req && req.abort) {
// FIXME: This is EVIL, but I can't think of a better way to expose
// abort() operations on underlying HTTP requests :(
reqPromise.abort = req.abort.bind(req);
}
}
catch (ex) {
} catch (ex) {
defer.reject(ex);
if (callback) {
callback(ex);
}
}
return reqPromise;
}
},
};
/*
@@ -577,25 +701,44 @@ module.exports.MatrixHttpApi.prototype = {
*
* If onlyData is true, the defer/callback is invoked with the body of the
* response, otherwise the result code.
*
* If parseErrorJson is true, we will JSON.parse the body if we get a 4xx error.
*
*/
var requestCallback = function(defer, userDefinedCallback, onlyData) {
const requestCallback = function(
defer, userDefinedCallback, onlyData,
parseErrorJson, bodyParser,
) {
userDefinedCallback = userDefinedCallback || function() {};
return function(err, response, body) {
if (!err && response.statusCode >= 400) {
err = new module.exports.MatrixError(body);
err.httpStatus = response.statusCode;
if (!err) {
try {
if (response.statusCode >= 400) {
if (parseErrorJson) {
// we won't have json-decoded the response.
body = JSON.parse(body);
}
err = new module.exports.MatrixError(body);
} else if (bodyParser) {
body = bodyParser(body);
}
} catch (e) {
err = e;
}
if (err) {
err.httpStatus = response.statusCode;
}
}
if (err) {
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);
+228
View File
@@ -0,0 +1,228 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/** @module interactive-auth */
const q = require("q");
const 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} 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.
*/
submitAuthDict: function(authData) {
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);
},
/**
* 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) {
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);
} 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() {
const nextStage = this._chooseStage();
if (!nextStage) {
throw new Error("No incomplete flows from the server");
}
let 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() {
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
*
* @private
* @return {object} flow
*/
_chooseFlow: function() {
const 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) {
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;

Some files were not shown because too many files have changed in this diff Show More