Compare commits

..

1234 Commits

Author SHA1 Message Date
Richard van der Hoff e8c6002d08 v0.7.5 2017-02-04 10:15:09 +00:00
Richard van der Hoff d9033812a2 Prepare changelog for v0.7.5 2017-02-04 10:15:02 +00:00
Richard van der Hoff 2e6b93f886 v0.7.5-rc.3 2017-02-03 15:24:28 +00:00
Richard van der Hoff afc4e145b6 Prepare changelog for v0.7.5-rc.3 2017-02-03 15:24:21 +00:00
Richard van der Hoff cee243a2a2 prep changelog 2017-02-03 15:21:15 +00:00
Richard van der Hoff 5fd74109ff Fix device list update
s/flushNewDeviceRequests/refreshOutdatedDeviceLists/ - this got fixed on one PR
and apparenlty I failed to merge the changes correctly
2017-02-03 14:29:50 +00:00
Richard van der Hoff a3cc8eb1f6 Include DeviceInfo in deviceVerificationChanged events
... to help the UI update itself
2017-02-03 14:27:08 +00:00
David Baker bd4de4832c v0.7.5-rc.2 2017-02-03 13:01:15 +00:00
David Baker 9e74c934a1 Prepare changelog for v0.7.5-rc.2 2017-02-03 13:01:14 +00:00
David Baker a056d4916a Prepare changelog for v0.7.5-rc.2 2017-02-03 13:00:26 +00:00
David Baker 31630859a2 Prepare changelog for v0.7.5-rc.2 2017-02-03 12:57:45 +00:00
David Baker 8cb41f6797 Merge remote-tracking branch 'origin/develop' into release-v0.7.5 2017-02-03 12:54:32 +00:00
Richard van der Hoff c3a8aeca42 Merge pull request #348 from matrix-org/rav/device_list_stream
Use the device change notifications interface
2017-02-03 12:49:33 +00:00
Richard van der Hoff eaa95fb1e5 Merge pull request #347 from matrix-org/rav/rewrite_device_query_logic
Rewrite the device key query logic
2017-02-03 12:49:11 +00:00
David Baker 8e6bca34d7 v0.7.5-rc.1 2017-02-03 12:05:50 +00:00
David Baker 1cdffa2c81 Prepare changelog for v0.7.5-rc.1 2017-02-03 12:05:50 +00:00
Richard van der Hoff 8441589ce6 Merge pull request #336 from matrix-org/matthew/blacklist-unverified
Support for blacklisting unverified devices, both per-room and globally
2017-02-03 10:26:59 +00:00
David Baker b52ba89cec Merge pull request #349 from matrix-org/matthew/track-event-error2
track errors when events can't be sent
2017-02-03 09:35:24 +00:00
Matthew Hodgson b99e1205c4 track errors when events can't send 2017-02-03 00:44:12 +00:00
Richard van der Hoff 8d502743a5 Refresh device list on startup
On initialsync, call the /keys/changes api to see which users have updated
their devices. (On failure, invalidate all of them).
2017-02-03 00:33:56 +00:00
Richard van der Hoff 732a764ec6 Refactor crypto initialsync handling
Pass a store into the Crypto object so that it doesn't need to make assumptions
about the EventEmitter, and use the new metadata on sync events to distinguish
between initialsyncs and normal syncs
2017-02-03 00:33:54 +00:00
Richard van der Hoff 9975786bac Store the token corresponding to the last device update in localstorage
... so that we can, in future, use it when restarting the client.
2017-02-03 00:32:24 +00:00
Richard van der Hoff 89ef4aa6e7 Handle device change notifications from /sync
When we get a notification from /sync that a user has updated their device
list, mark the list outdated, and then fire off a device query.
2017-02-03 00:32:16 +00:00
Richard van der Hoff 7e82ac3620 Merge branch 'develop' into rav/rewrite_device_query_logic 2017-02-03 00:12:46 +00:00
Richard van der Hoff c3440c506c Address review comments
Update some comments, and s/flushNewDeviceRequests/refreshOutdatedDeviceLists/.
2017-02-03 00:10:13 +00:00
Richard van der Hoff f16ef93cc3 package.json: Add .babelrc to files
... in the hope that this will mean it gets included when `npm install`ing
matrix-js-sdk into riot-web, and hence stopping babel picking up riot-web's
.babelrc.
2017-02-02 22:14:24 +00:00
Matthew Hodgson b6f3fc5466 Merge branch 'develop' into matthew/blacklist-unverified 2017-02-02 22:02:22 +00:00
Richard van der Hoff 6690f59410 Merge pull request #346 from matrix-org/rav/factor_out_devicelist
Factor out device list management
2017-02-02 19:40:27 +00:00
Richard van der Hoff 65e08b9633 Fix copyright 2017-02-02 19:39:40 +00:00
Matthew Hodgson 2e916e63f5 Merge pull request #335 from matrix-org/matthew/warn-unknown-devices
Support for warning users when unknown devices show up
2017-02-02 18:08:39 +00:00
Richard van der Hoff 94addb6315 Rewrite the device key query logic
Only permit one query per user at a time.
2017-02-02 13:49:43 +00:00
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
David Baker 02f8e7da3d 0.6.2 2016-10-05 16:43:11 +01:00
David Baker a245b735b3 Prepare changelog for v0.6.2 2016-10-05 16:39:48 +01:00
Richard van der Hoff 0f71983cb9 Merge pull request #221 from matrix-org/dbkr/check_dependencies
Check dependencies aren't on develop in release.sh
2016-10-05 16:38:41 +01:00
David Baker 7db6b9e490 Check dependencies aren't on develop in release.sh 2016-10-05 16:34:36 +01:00
David Baker 0021b21170 Merge pull request #220 from matrix-org/rav/fix_turnserver_leak
Fix checkTurnServers leak on logout
2016-10-03 11:24:25 +01:00
David Baker 3080dc018a Merge pull request #219 from matrix-org/rav/refactor_httpapi
Fix leak of file upload objects
2016-10-03 11:18:02 +01:00
David Baker f5d0ec32e5 Merge pull request #218 from matrix-org/rav/deduplicate_storage_update
crypto: remove duplicate code
2016-10-03 11:01:33 +01:00
Richard van der Hoff 6b3a06a8ed Fix checkTurnServers leak on logout
Remember to cancel the checkTurnServers callback when we stop the client.
2016-10-02 21:17:31 +01:00
Richard van der Hoff 9e89e71e0e Fix leak of file upload objects
After an upload completed, we were failing to delete the details of the upload
from the list (because the comparison was bogus), meaning we leaked an object
each time.

While we're in the area:

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

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

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

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

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

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

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

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

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

We should only be adding the raw event.

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

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

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

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

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

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

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

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

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

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

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

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

Further work here includes:

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

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

This achieves several aims:

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

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

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

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

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

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

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

Two things here:

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

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

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

To do this, we add a layer on top of window.setTimeout. We run a callback every
second, which then checks the wallclock time and runs any pending callbacks.
2016-03-31 13:51:18 +01:00
Richard van der Hoff bbe41aa7b4 0.5.1 2016-03-30 13:18:42 +01:00
Richard van der Hoff ece9391878 Prepare changelog for v0.5.1 2016-03-30 13:17:10 +01:00
David Baker 64640287cf Merge pull request #119 from matrix-org/dbkr/member_count_only_joined
Only count joined members for the member count in notifications.
2016-03-24 14:01:45 +00:00
David Baker 57f88b00ba d'oh, no es6 in js-sdk 2016-03-24 13:57:55 +00:00
David Baker e618ad9589 Only count joined members for the member count in notifications. Fixes https://github.com/vector-im/vector-web/issues/1067 2016-03-24 13:55:30 +00:00
Richard van der Hoff 69c4d7b66e Merge pull request #118 from matrix-org/dbkr/maysendevent
Add maySendEvent to match maySendStateEvent
2016-03-24 11:43:29 +00:00
David Baker d7b3b91eec Failed to remove extra param 2016-03-23 17:08:22 +00:00
David Baker 88cc63e2a2 Add maySendEvent to match maySendStateEvent. Make them use the same function internally. Also add convenience maySendMessage. Also tests. 2016-03-23 15:10:51 +00:00
Richard van der Hoff cdea96fa0a Release script tweaks
- be more helpful if update_changelog is not installed
- behave sanely if v is omitted on tag arg
2016-03-23 14:50:03 +00:00
Matthew Hodgson cfc10fa82d fix invite picker info again... 2016-03-23 12:03:56 +00:00
Richard van der Hoff 9d8973bf1f 0.5.0 2016-03-22 17:56:32 +00:00
Richard van der Hoff 7f95237e02 Prepare changelog for v0.5.0 2016-03-22 17:56:05 +00:00
Matthew Hodgson e1415d9829 Merge pull request #117 from matrix-org/matthew/roomlist
get/setRoomVisibility API
2016-03-22 12:30:18 +00:00
Richard van der Hoff 19a12b3c79 Merge pull request #115 from matrix-org/rav/txnid_clashes
Include a counter in generated transaction IDs
2016-03-22 12:20:02 +00:00
Matthew Hodgson bec41e4f94 incorporate review 2016-03-22 10:20:02 +00:00
Matthew Hodgson 5f177aeec4 get/setRoomVisibility API 2016-03-22 00:55:53 +00:00
Matthew Hodgson b422916452 add to existing users if present, to avoid destroying presence data 2016-03-21 18:15:55 +00:00
Matthew Hodgson 10378c0e7f Merge pull request #116 from matrix-org/matthew/fix-invite-picker-info
update store user metadata based on membership events rather than presence
2016-03-21 16:12:42 +00:00
Matthew Hodgson fba4d5fb0a Merge pull request #114 from matrix-org/matthew/stop-peeking
API to stop peeking
2016-03-21 16:12:29 +00:00
Matthew Hodgson 77101823f5 track kicked rooms correctly 2016-03-19 02:18:37 +00:00
Matthew Hodgson 15bc608368 presence no longer returns profile data, so we have to update our store's users based on membership events instead 2016-03-19 01:45:10 +00:00
Richard van der Hoff dfc4b34d09 Include a counter in generated transaction IDs
Fixes a flaky test which sometimes failed due to sending two events in the same
millisecond.
2016-03-18 21:32:15 +00:00
Richard van der Hoff ad9daecbd4 Pass the right options into SyncApi when peeking
When we peek into a room, we create its Room object. We need to make sure it is
created with the same options as we would if it were created via the /sync
calls.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Still TODO here:

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

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

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

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

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

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

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

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

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

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

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

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

Add stub tests for edge cases and implement test for the common case.
2015-10-16 11:32:27 +01:00
Kegan Dougal 43fc200dae Read receipt HTTP API tweaks 2015-10-16 09:36:13 +01:00
David Baker 6679e93afc Add untested read receipt sending method 2015-10-16 09:12:50 +01:00
105 changed files with 26927 additions and 40005 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"],
}
}
+7 -1
View File
@@ -6,4 +6,10 @@ coverage
lib-cov
out
reports
dist/browser-matrix-dev.js
/dist
/lib
/specbuild
# version file and tarball created by 'npm pack'
/git-revision.txt
/matrix-js-sdk-*.tgz
-11
View File
@@ -1,11 +0,0 @@
{
"node": true,
"jasmine": true,
"nonew": true,
"curly": true,
"forin": true,
"freeze": true,
"undef": true,
"unused": "vars"
}
+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
+679 -7
View File
@@ -1,3 +1,675 @@
Changes in [0.7.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.5) (2017-02-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.5-rc.3...v0.7.5)
No changes from 0.7.5-rc.3
Changes in [0.7.5-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.5-rc.3) (2017-02-03)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.5-rc.2...v0.7.5-rc.3)
* Include DeviceInfo in deviceVerificationChanged events
[a3cc8eb](https://github.com/matrix-org/matrix-js-sdk/commit/a3cc8eb1f6d165576a342596f638316721cb26b6)
* Fix device list update
[5fd7410](https://github.com/matrix-org/matrix-js-sdk/commit/5fd74109ffc56b73deb40c2604d84c38b8032c40)
Changes in [0.7.5-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.5-rc.2) (2017-02-03)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.5-rc.1...v0.7.5-rc.2)
* Use the device change notifications interface
[\#348](https://github.com/matrix-org/matrix-js-sdk/pull/348)
* Rewrite the device key query logic
[\#347](https://github.com/matrix-org/matrix-js-sdk/pull/347)
Changes in [0.7.5-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.5-rc.1) (2017-02-03)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.4...v0.7.5-rc.1)
* Support for blacklisting unverified devices, both per-room and globally
[\#336](https://github.com/matrix-org/matrix-js-sdk/pull/336)
* track errors when events can't be sent
[\#349](https://github.com/matrix-org/matrix-js-sdk/pull/349)
* Factor out device list management
[\#346](https://github.com/matrix-org/matrix-js-sdk/pull/346)
* Support for warning users when unknown devices show up
[\#335](https://github.com/matrix-org/matrix-js-sdk/pull/335)
* Enable sourcemaps in browserified distro
[\#345](https://github.com/matrix-org/matrix-js-sdk/pull/345)
* Record all e2e room settings in localstorage
[\#344](https://github.com/matrix-org/matrix-js-sdk/pull/344)
* Make Olm work with browserified js-sdk
[\#340](https://github.com/matrix-org/matrix-js-sdk/pull/340)
* Make browserify a dev dependency
[\#339](https://github.com/matrix-org/matrix-js-sdk/pull/339)
* Allow single line brace-style
[\#338](https://github.com/matrix-org/matrix-js-sdk/pull/338)
* Turn on comma-dangle for function calls
[\#333](https://github.com/matrix-org/matrix-js-sdk/pull/333)
* Add prefer-const
[\#331](https://github.com/matrix-org/matrix-js-sdk/pull/331)
* Support for importing and exporting megolm sessions
[\#326](https://github.com/matrix-org/matrix-js-sdk/pull/326)
* Fix linting on all tests
[\#329](https://github.com/matrix-org/matrix-js-sdk/pull/329)
* Fix ESLint warnings and errors
[\#325](https://github.com/matrix-org/matrix-js-sdk/pull/325)
* BREAKING CHANGE: Remove WebStorageStore
[\#324](https://github.com/matrix-org/matrix-js-sdk/pull/324)
Changes in [0.7.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.4) (2017-01-16)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.4-rc.1...v0.7.4)
* Fix non-conference calling
Changes in [0.7.4-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.4-rc.1) (2017-01-13)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.3...v0.7.4-rc.1)
* Remove babel-polyfill
[\#321](https://github.com/matrix-org/matrix-js-sdk/pull/321)
* Update build process for ES6
[\#320](https://github.com/matrix-org/matrix-js-sdk/pull/320)
* 'babel' is not a babel package anymore
[\#319](https://github.com/matrix-org/matrix-js-sdk/pull/319)
* Add Babel for ES6 support
[\#318](https://github.com/matrix-org/matrix-js-sdk/pull/318)
* Move screen sharing check/error
[\#317](https://github.com/matrix-org/matrix-js-sdk/pull/317)
* release.sh: Bail early if there are uncommitted changes
[\#316](https://github.com/matrix-org/matrix-js-sdk/pull/316)
Changes in [0.7.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.3) (2017-01-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.2...v0.7.3)
* User presence list feature
[\#310](https://github.com/matrix-org/matrix-js-sdk/pull/310)
* Allow clients the ability to set a default local timeout
[\#313](https://github.com/matrix-org/matrix-js-sdk/pull/313)
* Add API to delete threepid
[\#312](https://github.com/matrix-org/matrix-js-sdk/pull/312)
Changes in [0.7.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.2) (2016-12-15)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.1...v0.7.2)
* Bump to Olm 2.0
[\#309](https://github.com/matrix-org/matrix-js-sdk/pull/309)
* Sanity check payload length before encrypting
[\#307](https://github.com/matrix-org/matrix-js-sdk/pull/307)
* Remove dead _sendPingToDevice function
[\#308](https://github.com/matrix-org/matrix-js-sdk/pull/308)
* Add setRoomDirectoryVisibilityAppService
[\#306](https://github.com/matrix-org/matrix-js-sdk/pull/306)
* Update release script to do signed releases
[\#305](https://github.com/matrix-org/matrix-js-sdk/pull/305)
* e2e: Wait for pending device lists
[\#304](https://github.com/matrix-org/matrix-js-sdk/pull/304)
* Start a new megolm session when devices are blacklisted
[\#303](https://github.com/matrix-org/matrix-js-sdk/pull/303)
* E2E: Download our own devicelist on startup
[\#302](https://github.com/matrix-org/matrix-js-sdk/pull/302)
Changes in [0.7.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.1) (2016-12-09)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.1-rc.1...v0.7.1)
No changes
Changes in [0.7.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.1-rc.1) (2016-12-05)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.0...v0.7.1-rc.1)
* Avoid NPE when no sessionStore is given
[\#300](https://github.com/matrix-org/matrix-js-sdk/pull/300)
* Improve decryption error messages
[\#299](https://github.com/matrix-org/matrix-js-sdk/pull/299)
* Revert "Use native Array.isArray when available."
[\#283](https://github.com/matrix-org/matrix-js-sdk/pull/283)
* Use native Array.isArray when available.
[\#282](https://github.com/matrix-org/matrix-js-sdk/pull/282)
Changes in [0.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.7.0) (2016-11-18)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4...v0.7.0)
* Avoid a packetstorm of device queries on startup
[\#297](https://github.com/matrix-org/matrix-js-sdk/pull/297)
* E2E: Check devices to share keys with on each send
[\#295](https://github.com/matrix-org/matrix-js-sdk/pull/295)
* Apply unknown-keyshare mitigations
[\#296](https://github.com/matrix-org/matrix-js-sdk/pull/296)
* distinguish unknown users from deviceless users
[\#294](https://github.com/matrix-org/matrix-js-sdk/pull/294)
* Allow starting client with initialSyncLimit = 0
[\#293](https://github.com/matrix-org/matrix-js-sdk/pull/293)
* Make timeline-window _unpaginate public and rename to unpaginate
[\#289](https://github.com/matrix-org/matrix-js-sdk/pull/289)
* Send a STOPPED sync updated after call to stopClient
[\#286](https://github.com/matrix-org/matrix-js-sdk/pull/286)
* Fix bug in verifying megolm event senders
[\#292](https://github.com/matrix-org/matrix-js-sdk/pull/292)
* Handle decryption of events after they arrive
[\#288](https://github.com/matrix-org/matrix-js-sdk/pull/288)
* Fix examples.
[\#287](https://github.com/matrix-org/matrix-js-sdk/pull/287)
* Add a travis.yml
[\#278](https://github.com/matrix-org/matrix-js-sdk/pull/278)
* Encrypt all events, including 'm.call.*'
[\#277](https://github.com/matrix-org/matrix-js-sdk/pull/277)
* Ignore reshares of known megolm sessions
[\#276](https://github.com/matrix-org/matrix-js-sdk/pull/276)
* Log to the console on unknown session
[\#274](https://github.com/matrix-org/matrix-js-sdk/pull/274)
* Make it easier for SDK users to wrap prevailing the 'request' function
[\#273](https://github.com/matrix-org/matrix-js-sdk/pull/273)
Changes in [0.6.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4) (2016-11-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4-rc.2...v0.6.4)
* Change release script to pass version by environment variable
Changes in [0.6.4-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4-rc.2) (2016-11-02)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.4-rc.1...v0.6.4-rc.2)
* Add getRoomTags method to client
[\#236](https://github.com/matrix-org/matrix-js-sdk/pull/236)
Changes in [0.6.4-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.4-rc.1) (2016-11-02)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.3...v0.6.4-rc.1)
Breaking Changes
----------------
* Bundled version of the JS SDK are no longer versioned along with
source files in the dist/ directory. As of this release, they
will be included in the release tarball, but not the source
repository.
Other Changes
-------------
* More fixes to the release script
[\#272](https://github.com/matrix-org/matrix-js-sdk/pull/272)
* Update the release process to use github releases
[\#271](https://github.com/matrix-org/matrix-js-sdk/pull/271)
* Don't package the world when we release
[\#270](https://github.com/matrix-org/matrix-js-sdk/pull/270)
* Add ability to set a filter prior to the first /sync
[\#269](https://github.com/matrix-org/matrix-js-sdk/pull/269)
* Sign one-time keys, and verify their signatures
[\#243](https://github.com/matrix-org/matrix-js-sdk/pull/243)
* Check for duplicate message indexes for group messages
[\#241](https://github.com/matrix-org/matrix-js-sdk/pull/241)
* Rotate megolm sessions
[\#240](https://github.com/matrix-org/matrix-js-sdk/pull/240)
* Check recipient and sender in Olm messages
[\#239](https://github.com/matrix-org/matrix-js-sdk/pull/239)
* Consistency checks for E2E device downloads
[\#237](https://github.com/matrix-org/matrix-js-sdk/pull/237)
* Support User-Interactive auth for delete device
[\#235](https://github.com/matrix-org/matrix-js-sdk/pull/235)
* Utility to help with interactive auth
[\#234](https://github.com/matrix-org/matrix-js-sdk/pull/234)
* Fix sync breaking when an invalid filterId is in localStorage
[\#228](https://github.com/matrix-org/matrix-js-sdk/pull/228)
Changes in [0.6.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.3) (2016-10-12)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.2...v0.6.3)
Breaking Changes
----------------
* Add a 'RECONNECTING' state to the sync states. This is an additional state
between 'SYNCING' and 'ERROR', so most clients should not notice.
Other Changes
----------------
* Fix params getting replaced on register calls
[\#233](https://github.com/matrix-org/matrix-js-sdk/pull/233)
* Fix potential 30s delay on reconnect
[\#232](https://github.com/matrix-org/matrix-js-sdk/pull/232)
* uploadContent: Attempt some consistency between browser and node
[\#230](https://github.com/matrix-org/matrix-js-sdk/pull/230)
* Fix error handling on uploadContent
[\#229](https://github.com/matrix-org/matrix-js-sdk/pull/229)
* Fix uploadContent for node.js
[\#226](https://github.com/matrix-org/matrix-js-sdk/pull/226)
* Don't emit ERROR until a keepalive poke fails
[\#223](https://github.com/matrix-org/matrix-js-sdk/pull/223)
* Function to get the fallback url for interactive auth
[\#224](https://github.com/matrix-org/matrix-js-sdk/pull/224)
* Revert "Handle the first /sync failure differently."
[\#222](https://github.com/matrix-org/matrix-js-sdk/pull/222)
Changes in [0.6.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.2) (2016-10-05)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.1...v0.6.2)
* Check dependencies aren't on develop in release.sh
[\#221](https://github.com/matrix-org/matrix-js-sdk/pull/221)
* Fix checkTurnServers leak on logout
[\#220](https://github.com/matrix-org/matrix-js-sdk/pull/220)
* Fix leak of file upload objects
[\#219](https://github.com/matrix-org/matrix-js-sdk/pull/219)
* crypto: remove duplicate code
[\#218](https://github.com/matrix-org/matrix-js-sdk/pull/218)
* Add API for 3rd party location lookup
[\#217](https://github.com/matrix-org/matrix-js-sdk/pull/217)
* Handle the first /sync failure differently.
[\#216](https://github.com/matrix-org/matrix-js-sdk/pull/216)
Changes in [0.6.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.1) (2016-09-21)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.6.0...v0.6.1)
* Fix the ed25519 key checking
[\#215](https://github.com/matrix-org/matrix-js-sdk/pull/215)
* Add MatrixClient.getEventSenderDeviceInfo()
[\#214](https://github.com/matrix-org/matrix-js-sdk/pull/214)
Changes in [0.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.6.0) (2016-09-21)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.6...v0.6.0)
* Pull user device list on join
[\#212](https://github.com/matrix-org/matrix-js-sdk/pull/212)
* Fix sending of oh_hais on bad sessions
[\#213](https://github.com/matrix-org/matrix-js-sdk/pull/213)
* Support /publicRooms pagination
[\#211](https://github.com/matrix-org/matrix-js-sdk/pull/211)
* Update the olm library version to 1.3.0
[\#205](https://github.com/matrix-org/matrix-js-sdk/pull/205)
* Comment what the logic in uploadKeys does
[\#209](https://github.com/matrix-org/matrix-js-sdk/pull/209)
* Include keysProved and keysClaimed in the local echo for events we send.
[\#210](https://github.com/matrix-org/matrix-js-sdk/pull/210)
* Check if we need to upload new one-time keys every 10 minutes
[\#208](https://github.com/matrix-org/matrix-js-sdk/pull/208)
* Reset oneTimeKey to null on each loop iteration.
[\#207](https://github.com/matrix-org/matrix-js-sdk/pull/207)
* Add getKeysProved and getKeysClaimed methods to MatrixEvent.
[\#206](https://github.com/matrix-org/matrix-js-sdk/pull/206)
* Send a 'm.new_device' when we get a message for an unknown group session
[\#204](https://github.com/matrix-org/matrix-js-sdk/pull/204)
* Introduce EventTimelineSet, filtered timelines and global notif timeline.
[\#196](https://github.com/matrix-org/matrix-js-sdk/pull/196)
* Wrap the crypto event handlers in try/catch blocks
[\#203](https://github.com/matrix-org/matrix-js-sdk/pull/203)
* Show warnings on to-device decryption fail
[\#202](https://github.com/matrix-org/matrix-js-sdk/pull/202)
* s/Displayname/DisplayName/
[\#201](https://github.com/matrix-org/matrix-js-sdk/pull/201)
* OH HAI
[\#200](https://github.com/matrix-org/matrix-js-sdk/pull/200)
* Share the current ratchet with new members
[\#199](https://github.com/matrix-org/matrix-js-sdk/pull/199)
* Move crypto bits into a subdirectory
[\#198](https://github.com/matrix-org/matrix-js-sdk/pull/198)
* Refactor event handling in Crypto
[\#197](https://github.com/matrix-org/matrix-js-sdk/pull/197)
* Don't create Olm sessions proactively
[\#195](https://github.com/matrix-org/matrix-js-sdk/pull/195)
* Use to-device events for key sharing
[\#194](https://github.com/matrix-org/matrix-js-sdk/pull/194)
* README: callbacks deprecated
[\#193](https://github.com/matrix-org/matrix-js-sdk/pull/193)
* Fix sender verification for megolm messages
[\#192](https://github.com/matrix-org/matrix-js-sdk/pull/192)
* Use `ciphertext` instead of `body` in megolm events
[\#191](https://github.com/matrix-org/matrix-js-sdk/pull/191)
* Add debug methods to get the state of OlmSessions
[\#189](https://github.com/matrix-org/matrix-js-sdk/pull/189)
* MatrixClient.getStoredDevicesForUser
[\#190](https://github.com/matrix-org/matrix-js-sdk/pull/190)
* Olm-related cleanups
[\#188](https://github.com/matrix-org/matrix-js-sdk/pull/188)
* Update to fixed olmlib
[\#187](https://github.com/matrix-org/matrix-js-sdk/pull/187)
* always play audio out of the remoteAudioElement if it exists.
[\#186](https://github.com/matrix-org/matrix-js-sdk/pull/186)
* Fix exceptions where HTMLMediaElement loads and plays race
[\#185](https://github.com/matrix-org/matrix-js-sdk/pull/185)
* Reset megolm session when people join/leave the room
[\#183](https://github.com/matrix-org/matrix-js-sdk/pull/183)
* Fix exceptions when dealing with redactions
[\#184](https://github.com/matrix-org/matrix-js-sdk/pull/184)
Changes in [0.5.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.6) (2016-08-28)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.5...v0.5.6)
* Put all of the megolm keys in one room message
[\#182](https://github.com/matrix-org/matrix-js-sdk/pull/182)
* Reinstate device blocking for simple Olm
[\#181](https://github.com/matrix-org/matrix-js-sdk/pull/181)
* support for unpacking megolm keys
[\#180](https://github.com/matrix-org/matrix-js-sdk/pull/180)
* Send out megolm keys when we start a megolm session
[\#179](https://github.com/matrix-org/matrix-js-sdk/pull/179)
* Change the result structure for ensureOlmSessionsForUsers
[\#178](https://github.com/matrix-org/matrix-js-sdk/pull/178)
* Factor out a function for doing olm encryption
[\#177](https://github.com/matrix-org/matrix-js-sdk/pull/177)
* Move DeviceInfo and DeviceVerification to separate module
[\#175](https://github.com/matrix-org/matrix-js-sdk/pull/175)
* Make encryption asynchronous
[\#176](https://github.com/matrix-org/matrix-js-sdk/pull/176)
* Added ability to set and get status_msg for presence.
[\#167](https://github.com/matrix-org/matrix-js-sdk/pull/167)
* Megolm: don't dereference nullable object
[\#174](https://github.com/matrix-org/matrix-js-sdk/pull/174)
* Implement megolm encryption/decryption
[\#173](https://github.com/matrix-org/matrix-js-sdk/pull/173)
* Update our push rules when they come down stream
[\#170](https://github.com/matrix-org/matrix-js-sdk/pull/170)
* Factor Olm encryption/decryption out to new classes
[\#172](https://github.com/matrix-org/matrix-js-sdk/pull/172)
* Make DeviceInfo more useful, and refactor crypto methods to use it
[\#171](https://github.com/matrix-org/matrix-js-sdk/pull/171)
* Move login and register methods into base-apis
[\#169](https://github.com/matrix-org/matrix-js-sdk/pull/169)
* Remove defaultDeviceDisplayName from MatrixClient options
[\#168](https://github.com/matrix-org/matrix-js-sdk/pull/168)
Changes in [0.5.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.5) (2016-08-11)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.4...v0.5.5)
* Add room.getAliases() and room.getCanonicalAlias
* Add API calls `/register/email/requestToken`, `/account/password/email/requestToken` and `/account/3pid/email/requestToken`
* Add `User.currentlyActive` and `User.lastPresenceTs` events for changes in fields on the User object
* Add `logout` and `deactivateAccount`
* Make sure we actually stop the sync loop on logout
[\#166](https://github.com/matrix-org/matrix-js-sdk/pull/166)
* Zero is a valid power level
[\#164](https://github.com/matrix-org/matrix-js-sdk/pull/164)
* Verify e2e keys on download
[\#163](https://github.com/matrix-org/matrix-js-sdk/pull/163)
* Factor crypto stuff out of MatrixClient
[\#162](https://github.com/matrix-org/matrix-js-sdk/pull/162)
* Refactor device key upload
[\#161](https://github.com/matrix-org/matrix-js-sdk/pull/161)
* Wrappers for devices API
[\#158](https://github.com/matrix-org/matrix-js-sdk/pull/158)
* Add deactivate account function
[\#160](https://github.com/matrix-org/matrix-js-sdk/pull/160)
* client.listDeviceKeys: Expose device display name
[\#159](https://github.com/matrix-org/matrix-js-sdk/pull/159)
* Add `logout`
[\#157](https://github.com/matrix-org/matrix-js-sdk/pull/157)
* Fix email registration
[\#156](https://github.com/matrix-org/matrix-js-sdk/pull/156)
* Factor out MatrixClient methods to MatrixBaseApis
[\#155](https://github.com/matrix-org/matrix-js-sdk/pull/155)
* Fix some broken tests
[\#154](https://github.com/matrix-org/matrix-js-sdk/pull/154)
* make jenkins fail the build if the tests fail
[\#153](https://github.com/matrix-org/matrix-js-sdk/pull/153)
* deviceId-related fixes
[\#152](https://github.com/matrix-org/matrix-js-sdk/pull/152)
* /login, /register: Add device_id and initial_device_display_name
[\#151](https://github.com/matrix-org/matrix-js-sdk/pull/151)
* Support global account_data
[\#150](https://github.com/matrix-org/matrix-js-sdk/pull/150)
* Add more events to User
[\#149](https://github.com/matrix-org/matrix-js-sdk/pull/149)
* Add API calls for other requestToken endpoints
[\#148](https://github.com/matrix-org/matrix-js-sdk/pull/148)
* Add register-specific request token endpoint
[\#147](https://github.com/matrix-org/matrix-js-sdk/pull/147)
* Set a valid SPDX license identifier in package.json
[\#139](https://github.com/matrix-org/matrix-js-sdk/pull/139)
* Configure encryption on m.room.encryption events
[\#145](https://github.com/matrix-org/matrix-js-sdk/pull/145)
* Implement device blocking
[\#146](https://github.com/matrix-org/matrix-js-sdk/pull/146)
* Clearer doc for setRoomDirectoryVisibility
[\#144](https://github.com/matrix-org/matrix-js-sdk/pull/144)
* crypto: use memberlist to derive recipient list
[\#143](https://github.com/matrix-org/matrix-js-sdk/pull/143)
* Support for marking devices as unverified
[\#142](https://github.com/matrix-org/matrix-js-sdk/pull/142)
* Add Olm as an optionalDependency
[\#141](https://github.com/matrix-org/matrix-js-sdk/pull/141)
* Add room.getAliases() and room.getCanonicalAlias()
[\#140](https://github.com/matrix-org/matrix-js-sdk/pull/140)
* Change how MatrixEvent manages encrypted events
[\#138](https://github.com/matrix-org/matrix-js-sdk/pull/138)
* Catch exceptions when encrypting events
[\#137](https://github.com/matrix-org/matrix-js-sdk/pull/137)
* Support for marking devices as verified
[\#136](https://github.com/matrix-org/matrix-js-sdk/pull/136)
* Various matrix-client refactorings and fixes
[\#134](https://github.com/matrix-org/matrix-js-sdk/pull/134)
Changes in [0.5.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.4) (2016-06-02)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.3...v0.5.4)
* Correct fix for https://github.com/vector-im/vector-web/issues/1039
* Make release.sh work on OSX
Changes in [0.5.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.3) (2016-06-02)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.2...v0.5.3)
* Add support for the openid interface
[\#133](https://github.com/matrix-org/matrix-js-sdk/pull/133)
* Bugfix for HTTP upload content when running on node
[\#129](https://github.com/matrix-org/matrix-js-sdk/pull/129)
* Ignore missing profile (displayname and avatar_url) fields on
presence events, rather than overwriting existing valid profile
data from membership events or elsewhere.
Fixes https://github.com/vector-im/vector-web/issues/1039
Changes in [0.5.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.2) (2016-04-19)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.1...v0.5.2)
* Track the absolute time that presence events are received, so that the
relative lastActiveAgo value is meaningful.
[\#128](https://github.com/matrix-org/matrix-js-sdk/pull/128)
* Refactor the addition of events to rooms
[\#127](https://github.com/matrix-org/matrix-js-sdk/pull/127)
* Clean up test shutdown
[\#126](https://github.com/matrix-org/matrix-js-sdk/pull/126)
* Add methods to get (and set) pushers
[\#125](https://github.com/matrix-org/matrix-js-sdk/pull/125)
* URL previewing support
[\#122](https://github.com/matrix-org/matrix-js-sdk/pull/122)
* Avoid paginating forever in private rooms
[\#124](https://github.com/matrix-org/matrix-js-sdk/pull/124)
* Fix a bug where we recreated sync filters
[\#123](https://github.com/matrix-org/matrix-js-sdk/pull/123)
* Implement HTTP timeouts in realtime
[\#121](https://github.com/matrix-org/matrix-js-sdk/pull/121)
Changes in [0.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.1) (2016-03-30)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.5.0...v0.5.1)
* Only count joined members for the member count in notifications.
[\#119](https://github.com/matrix-org/matrix-js-sdk/pull/119)
* Add maySendEvent to match maySendStateEvent
[\#118](https://github.com/matrix-org/matrix-js-sdk/pull/118)
Changes in [0.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.5.0) (2016-03-22)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.4.2...v0.5.0)
**BREAKING CHANGES**:
* `opts.pendingEventOrdering`==`end` is no longer supported in the arguments to
`MatrixClient.startClient()`. Instead we provide a `detached` option, which
puts pending events into a completely separate list in the Room, accessible
via `Room.getPendingEvents()`.
[\#111](https://github.com/matrix-org/matrix-js-sdk/pull/111)
Other improvements:
* Log the stack when we get a sync error
[\#109](https://github.com/matrix-org/matrix-js-sdk/pull/109)
* Refactor transmitted-messages code
[\#110](https://github.com/matrix-org/matrix-js-sdk/pull/110)
* Add a method to the js sdk to look up 3pids on the ID server.
[\#113](https://github.com/matrix-org/matrix-js-sdk/pull/113)
* Support for cancelling pending events
[\#112](https://github.com/matrix-org/matrix-js-sdk/pull/112)
* API to stop peeking
[\#114](https://github.com/matrix-org/matrix-js-sdk/pull/114)
* update store user metadata based on membership events rather than presence
[\#116](https://github.com/matrix-org/matrix-js-sdk/pull/116)
* Include a counter in generated transaction IDs
[\#115](https://github.com/matrix-org/matrix-js-sdk/pull/115)
* get/setRoomVisibility API
[\#117](https://github.com/matrix-org/matrix-js-sdk/pull/117)
Changes in [0.4.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.4.2) (2016-03-17)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.4.1...v0.4.2)
* Try again if a pagination request gives us no new messages
[\#98](https://github.com/matrix-org/matrix-js-sdk/pull/98)
* Add a delay before we start polling the connectivity check endpoint
[\#99](https://github.com/matrix-org/matrix-js-sdk/pull/99)
* Clean up a codepath that was only used for crypto messages
[\#101](https://github.com/matrix-org/matrix-js-sdk/pull/101)
* Add maySendStateEvent method, ported from react-sdk (but fixed).
[\#94](https://github.com/matrix-org/matrix-js-sdk/pull/94)
* Add Session.logged_out event
[\#100](https://github.com/matrix-org/matrix-js-sdk/pull/100)
* make presence work when peeking.
[\#103](https://github.com/matrix-org/matrix-js-sdk/pull/103)
* Add RoomState.mayClientSendStateEvent()
[\#104](https://github.com/matrix-org/matrix-js-sdk/pull/104)
* Fix displaynames for member join events
[\#108](https://github.com/matrix-org/matrix-js-sdk/pull/108)
Changes in 0.4.1
================
Improvements:
* Check that `/sync` filters are correct before reusing them, and recreate
them if not (https://github.com/matrix-org/matrix-js-sdk/pull/85).
* Fire a `Room.timelineReset` event when a room's timeline is reset by a gappy
`/sync` (https://github.com/matrix-org/matrix-js-sdk/pull/87,
https://github.com/matrix-org/matrix-js-sdk/pull/93).
* Make `TimelineWindow.load()` faster in the simple case of loading the live
timeline (https://github.com/matrix-org/matrix-js-sdk/pull/88).
* Update room-name calculation code to use the name of the sender of the
invite when invited to a room
(https://github.com/matrix-org/matrix-js-sdk/pull/89).
* Don't reset the timeline when we join a room after peeking into it
(https://github.com/matrix-org/matrix-js-sdk/pull/91).
* Fire `Room.localEchoUpdated` events as local echoes progress through their
transmission process (https://github.com/matrix-org/matrix-js-sdk/pull/95,
https://github.com/matrix-org/matrix-js-sdk/pull/97).
* Avoid getting stuck in a pagination loop when the server sends us only
messages we've already seen
(https://github.com/matrix-org/matrix-js-sdk/pull/96).
New methods:
* Add `MatrixClient.setPushRuleActions` to set the actions for a push
notification rule (https://github.com/matrix-org/matrix-js-sdk/pull/90)
* Add `RoomState.maySendStateEvent` which determines if a given user has
permission to send a state event
(https://github.com/matrix-org/matrix-js-sdk/pull/94)
Changes in 0.4.0
================
**BREAKING CHANGES**:
* `RoomMember.getAvatarUrl()` and `MatrixClient.mxcUrlToHttp()` now return the
empty string when given anything other than an mxc:// URL. This ensures that
clients never inadvertantly reference content directly, leaking information
to third party servers. The `allowDirectLinks` option is provided if the client
wants to allow such links.
* Add a 'bindEmail' option to register()
Improvements:
* Support third party invites
* More appropriate naming for third party invite rooms
* Poll the 'versions' endpoint to re-establish connectivity
* Catch exceptions when syncing
* Room tag support
* Generate implicit read receipts
* Support CAS login
* Guest access support
* Never return non-mxc URLs by default
* Ability to cancel file uploads
* Use the Matrix C/S API v2 with r0 prefix
* Account data support
* Support non-contiguous event timelines
* Support new unread counts
* Local echo for read-receipts
New methods:
* Add method to fetch URLs not on the home or identity server
* Method to get the last receipt for a user
* Method to get all known users
* Method to delete an alias
Changes in 0.3.0
================
* `MatrixClient.getAvatarUrlForMember` has been removed and replaced with
`RoomMember.getAvatarUrl`. Arguments remain the same except the homeserver
URL must now be supplied from `MatrixClient.getHomeserverUrl()`.
```javascript
// before
var url = client.getAvatarUrlForMember(member, width, height, resize, allowDefault)
// after
var url = member.getAvatarUrl(client.getHomeserverUrl(), width, height, resize, allowDefault)
```
* `MatrixClient.getAvatarUrlForRoom` has been removed and replaced with
`Room.getAvatarUrl`. Arguments remain the same except the homeserver
URL must now be supplied from `MatrixClient.getHomeserverUrl()`.
```javascript
// before
var url = client.getAvatarUrlForRoom(room, width, height, resize, allowDefault)
// after
var url = room.getAvatarUrl(client.getHomeserverUrl(), width, height, resize, allowDefault)
```
* `s/Room.getMembersWithMemership/Room.getMembersWithMem`b`ership/g`
New methods:
* Added support for sending receipts via
`MatrixClient.sendReceipt(event, receiptType, callback)` and
`MatrixClient.sendReadReceipt(event, callback)`.
* Added support for receiving receipts via
`Room.getReceiptsForEvent(event)` and `Room.getUsersReadUpTo(event)`. Receipts
can be directly added to a `Room` using `Room.addReceipt(event)` though the
`MatrixClient` does this for you.
* Added support for muting local video and audio via the new methods
`MatrixCall.setMicrophoneMuted()`, `MatrixCall.isMicrophoneMuted(muted)`,
`MatrixCall.isLocalVideoMuted()` and `Matrix.setLocalVideoMuted(muted)`.
* Added **experimental** support for screen-sharing in Chrome via
`MatrixCall.placeScreenSharingCall(remoteVideoElement, localVideoElement)`.
* Added ability to perform server-side searches using
`MatrixClient.searchMessageText(opts)` and `MatrixClient.search(opts)`.
Improvements:
* Improve the performance of initial sync processing from `O(n^2)` to `O(n)`.
* `Room.name` will now take into account `m.room.canonical_alias` events.
* `MatrixClient.startClient` now takes an Object `opts` rather than a Number in
a backwards-compatible way. This `opts` allows syncing configuration options
to be specified including `includeArchivedRooms` and `resolveInvitesToProfiles`.
* `Room` objects which represent room invitations will now have state populated
from `invite_room_state` if it is included in the `m.room.member` event.
* `Room.getAvatarUrl` will now take into account `m.room.avatar` events.
Changes in 0.2.2
================
@@ -22,6 +694,10 @@ New methods:
Changes in 0.2.1
================
**BREAKING CHANGES**
* `MatrixClient.joinRoom` has changed from `(roomIdOrAlias, callback)` to
`(roomIdOrAlias, opts, callback)`.
Bug fixes:
* The `Content-Type` of file uploads is now explicitly set, without relying
on the browser to do it for us.
@@ -33,10 +709,6 @@ Improvements:
* There is now a try/catch block around the `request` function which will
reject/errback appropriately if an exception is thrown synchronously in it.
Breaking changes:
* `MatrixClient.joinRoom` has changed from `(roomIdOrAlias, callback)` to
`(roomIdOrAlias, opts, callback)`.
New methods:
* `MatrixClient.createAlias(alias, roomId)`
* `MatrixClient.getRoomIdForAlias(alias)`
@@ -57,7 +729,7 @@ Modified methods:
Changes in 0.2.0
================
Breaking changes:
**BREAKING CHANGES**:
* `MatrixClient.setPowerLevel` now expects a `MatrixEvent` and not an `Object`
for the `event` parameter.
@@ -88,7 +760,7 @@ New methods:
* `MatrixClient.mxcUrlToHttp(url, w, h, method)`
* `MatrixClient.getAvatarUrlForRoom(room, w, h, method)`
* `MatrixClient.uploadContent(file, callback)`
* `Room.getMembersWithMemership(membership)`
* `Room.getMembersWithMembership(membership)`
* `MatrixScheduler.getQueueForEvent(event)`
* `MatrixScheduler.removeEventFromQueue(event)`
* `$DATA_STORE.setSyncToken(token)`
@@ -123,7 +795,7 @@ Bug fixes:
Changes in 0.1.1
================
Breaking changes:
**BREAKING CHANGES**:
* `Room.calculateRoomName` is now private. Use `Room.recalculate` instead, and
access the calculated name via `Room.name`.
* `new MatrixClient(...)` no longer creates a `MatrixInMemoryStore` if
+4
View File
@@ -0,0 +1,4 @@
Contributing code to matrix-js-sdk
==================================
matrix-js-sdk follows the same pattern as https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst
+38 -31
View File
@@ -10,11 +10,12 @@ Quickstart
In a browser
------------
Copy ``dist/$VERSION/browser-matrix-$VERSION.js`` and add that as a ``<script>`` to
your page. There will be a global variable ``matrixcs`` attached to
``window`` through which you can access the SDK.
Download either the full or minified version from
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
``<script>`` to your page. There will be a global variable ``matrixcs``
attached to ``window`` through which you can access the SDK.
Please check [the working browser example](examples/browser) for more information.
Please check [the working browser example](examples/browser) for more information.
In Node.js
----------
@@ -64,6 +65,7 @@ Later versions of the SDK will:
Usage
=====
Conventions
-----------
@@ -78,7 +80,7 @@ are updated.
client.on("event", function(event) {
console.log(event.getType());
});
// Listen for typing changes
client.on("RoomMember.typing", function(event, member) {
if (member.typing) {
@@ -88,38 +90,43 @@ are updated.
console.log(member.name + " stopped typing.");
}
});
// start the client to setup the connection to the server
client.startClient();
```
### Promises or Callbacks
### Promises and Callbacks
The SDK supports *both* callbacks and Promises (Q). The convention
you'll see used is:
Most of the methods in the SDK are asynchronous: they do not directly return a
result, but instead return a [Promise](http://documentup.com/kriskowal/q/)
which will be fulfilled in the future.
The typical usage is something like:
```javascript
var promise = matrixClient.someMethod(arg1, arg2, callback);
```
The ``callback`` parameter is optional, so you could do:
```javascript
matrixClient.someMethod(arg1, arg2).then(function(err, result) {
matrixClient.someMethod(arg1, arg2).done(function(result) {
...
});
```
Alternatively, you could do:
Alternatively, if you have a Node.js-style ``callback(err, result)`` function,
you can pass the result of the promise into it with something like:
```javascript
matrixClient.someMethod(arg1, arg2, function(result) {
...
});
matrixClient.someMethod(arg1, arg2).nodeify(callback);
```
Methods which support this will be clearly marked as returning
``Promises``.
The main thing to note is that it is an error to discard the result of a
promise-returning function, as that will cause exceptions to go unobserved. If
you have nothing better to do with the result, just call ``.done()`` on it. See
http://documentup.com/kriskowal/q/#the-end for more information.
Methods which return a promise show this in their documentation.
Many methods in the SDK support *both* Node.js-style callbacks *and* Promises,
via an optional ``callback`` argument. The callback support is now deprecated:
new methods do not include a ``callback`` argument, and in the future it may be
removed from existing methods.
Examples
--------
@@ -147,10 +154,10 @@ core functionality of the SDK. These examples assume the SDK is setup like this:
});
}
});
matrixClient.startClient();
```
### Print out messages for all rooms
```javascript
@@ -166,7 +173,7 @@ core functionality of the SDK. These examples assume the SDK is setup like this:
"(%s) %s :: %s", room.name, event.getSender(), event.getContent().body
);
});
matrixClient.startClient();
```
@@ -198,10 +205,10 @@ Output:
);
}
});
matrixClient.startClient();
```
Output:
```
My Room
@@ -211,7 +218,7 @@ Output:
(join) Bob
(invite) @charlie:localhost
```
API Reference
=============
@@ -226,7 +233,7 @@ host the API reference from the source files like this:
$ cd .jsdoc
$ python -m SimpleHTTPServer 8005
```
Then visit ``http://localhost:8005`` to see the API docs.
Contributing
@@ -256,7 +263,7 @@ To run tests (Jasmine)::
```
$ npm test
```
To run linting:
```
$ npm run lint
-5826
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-6490
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-9900
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
-10023
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
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
+24
View File
@@ -0,0 +1,24 @@
#!/bin/sh
#
# pre-commit: script to run checks on a working copy before commit
#
# To use, symlink it into .git/hooks:
# ln -s ../../git-hooks/pre-commit .git/hooks
#
set -e
# create a temp dir
tmpdir=`mktemp -d`
trap 'rm -rf "$tmpdir"' EXIT
# get a copy of the index
git checkout-index --prefix="$tmpdir/" -a
# keep node_modules/.bin on the path
rootdir=`git rev-parse --show-toplevel`
export PATH="$rootdir/node_modules/.bin:$PATH"
# now run our checks
cd "$tmpdir"
npm run lint
+3
View File
@@ -1,3 +1,6 @@
var matrixcs = require("./lib/matrix");
matrixcs.request(require("request"));
module.exports = matrixcs;
var utils = require("./lib/utils");
utils.runPolyfills();
Executable
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash -l
set -x
export NVM_DIR="/home/jenkins/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 6 || exit $?
npm install || exit $?
RC=0
function fail {
echo $@ >&2
RC=1
}
npm test || fail "npm test 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
npm pack ||
fail "npm pack finished with return code $?"
npm run gendoc || fail "JSDoc failed with code $?"
exit $RC
-2602
View File
File diff suppressed because it is too large Load Diff
-461
View File
@@ -1,461 +0,0 @@
"use strict";
/**
* This is an internal module. See {@link MatrixHttpApi} for the public class.
* @module http-api
*/
var q = require("q");
var utils = require("./utils");
/*
TODO:
- CS: complete register function (doing stages)
- Identity server: linkEmail, authEmail, bindEmail, lookup3pid
*/
/**
* A constant representing the URI path for version 1 of the Client-Server HTTP API.
*/
module.exports.PREFIX_V1 = "/_matrix/client/api/v1";
/**
* A constant representing the URI path for version 2 alpha of the Client-Server
* HTTP API.
*/
module.exports.PREFIX_V2_ALPHA = "/_matrix/client/v2_alpha";
/**
* URI path for the identity API
*/
module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
/**
* Construct a MatrixHttpApi.
* @constructor
* @param {Object} opts The options to use for this HTTP API.
* @param {string} opts.baseUrl Required. The base client-server URL e.g.
* 'http://localhost:8008'.
* @param {Function} opts.request Required. The function to call for HTTP
* requests. This function must look like function(opts, callback){ ... }.
* @param {string} opts.prefix Required. The matrix client prefix to use, e.g.
* '/_matrix/client/api/v1'. See PREFIX_V1 and PREFIX_V2_ALPHA for constants.
* @param {bool} opts.onlyData True to return only the 'data' component of the
* response (e.g. the parsed HTTP body). If false, requests will return status
* codes and headers in addition to data. Default: false.
* @param {string} opts.accessToken The access_token to send with requests. Can be
* null to not send an access token.
* @param {Object} opts.extraParams Optional. Extra query parameters to send on
* requests.
*/
module.exports.MatrixHttpApi = function MatrixHttpApi(opts) {
utils.checkObjectHasKeys(opts, ["baseUrl", "request", "prefix"]);
opts.onlyData = opts.onlyData || false;
this.opts = opts;
};
module.exports.MatrixHttpApi.prototype = {
// URI functions
// =============
/**
* Get the HTTP URL for an MXC URI.
* @param {string} mxc The mxc:// URI.
* @param {Number} width The desired width of the thumbnail.
* @param {Number} height The desired height of the thumbnail.
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @return {string} The complete URL to the content.
*/
getHttpUriForMxc: function(mxc, width, height, resizeMethod) {
if (typeof mxc !== "string" || !mxc) {
return mxc;
}
if (mxc.indexOf("mxc://") !== 0) {
return mxc;
}
var serverAndMediaId = mxc.slice(6); // strips mxc://
var prefix = "/_matrix/media/v1/download/";
var params = {};
if (width) {
params.width = width;
}
if (height) {
params.height = height;
}
if (resizeMethod) {
params.method = resizeMethod;
}
if (utils.keys(params).length > 0) {
// these are thumbnailing params so they probably want the
// thumbnailing API...
prefix = "/_matrix/media/v1/thumbnail/";
}
var fragmentOffset = serverAndMediaId.indexOf("#"),
fragment = "";
if (fragmentOffset >= 0) {
fragment = serverAndMediaId.substr(fragmentOffset);
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
}
return this.opts.baseUrl + prefix + serverAndMediaId +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params))) + fragment;
},
/**
* Get an identicon URL from an arbitrary string.
* @param {string} identiconString The string to create an identicon for.
* @param {Number} width The desired width of the image in pixels.
* @param {Number} height The desired height of the image in pixels.
* @return {string} The complete URL to the identicon.
*/
getIdenticonUri: function(identiconString, width, height) {
if (!identiconString) {
return;
}
if (!width) { width = 96; }
if (!height) { height = 96; }
var params = {
width: width,
height: height
};
var path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
$ident: identiconString
});
return this.opts.baseUrl + path +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params)));
},
/**
* Get the content repository url with query parameters.
* @return {Object} An object with a 'base', 'path' and 'params' for base URL,
* path and query parameters respectively.
*/
getContentUri: function() {
var params = {
access_token: this.opts.accessToken
};
return {
base: this.opts.baseUrl,
path: "/_matrix/media/v1/upload",
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},
*/
uploadContent: function(file, callback) {
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
);
}
var defer = q.defer();
var url = this.opts.baseUrl + "/_matrix/media/v1/upload";
// 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
// browser-request base64 encode and then decode it again, we just
// use XMLHttpRequest directly.
// (browser-request doesn't support progress either, which is also kind
// of important here)
if (global.XMLHttpRequest) {
var xhr = new global.XMLHttpRequest();
var cb = requestCallback(defer, callback, this.opts.onlyData);
var timeout_fn = function() {
xhr.abort();
cb(new Error('Timeout'));
};
xhr.timeout_timer = setTimeout(timeout_fn, 30000);
xhr.onreadystatechange = function() {
switch (xhr.readyState) {
case global.XMLHttpRequest.DONE:
clearTimeout(xhr.timeout_timer);
var resp = JSON.parse(xhr.responseText);
if (resp.content_uri === undefined) {
cb(new Error('Bad response'));
return;
}
cb(undefined, xhr, resp.content_uri);
break;
}
};
xhr.upload.addEventListener("progress", function(ev) {
clearTimeout(xhr.timeout_timer);
xhr.timeout_timer = setTimeout(timeout_fn, 30000);
defer.notify(ev);
});
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
url += "&filename=" + encodeURIComponent(file.name);
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);
} else {
var queryParams = {
filename: file.name,
access_token: this.opts.accessToken
};
file.stream.pipe(
this.opts.request({
uri: url,
qs: queryParams,
method: "POST"
}, requestCallback(defer, callback, this.opts.onlyData))
);
}
return defer.promise;
},
idServerRequest: function(callback, method, path, params, prefix) {
var fullUri = this.opts.idBaseUrl + prefix + path;
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
);
}
var opts = {
uri: fullUri,
method: method,
withCredentials: false,
json: false,
_matrix_opts: this.opts
};
if (method == 'GET') {
opts.qs = params;
} else {
opts.form = params;
}
var defer = q.defer();
this.opts.request(
opts,
requestCallback(defer, callback, this.opts.onlyData)
);
return defer.promise;
},
/**
* Perform an authorised request to the homeserver.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
authedRequest: function(callback, method, path, queryParams, data) {
if (!queryParams) { queryParams = {}; }
queryParams.access_token = this.opts.accessToken;
return this.request(callback, method, path, queryParams, data);
},
/**
* Perform a request to the homeserver without any credentials.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
request: function(callback, method, path, queryParams, data) {
return this.requestWithPrefix(
callback, method, path, queryParams, data, this.opts.prefix
);
},
/**
* Perform an authorised request to the homeserver with a specific path
* prefix which overrides the default for this call only. Useful for hitting
* different Matrix Client-Server versions.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
authedRequestWithPrefix: function(callback, method, path, queryParams, data,
prefix) {
var fullUri = this.opts.baseUrl + prefix + path;
if (!queryParams) {
queryParams = {};
}
queryParams.access_token = this.opts.accessToken;
return this._request(callback, method, fullUri, queryParams, data);
},
/**
* Perform a request to the homeserver without any credentials but with a
* specific path prefix which overrides the default for this call only.
* Useful for hitting different Matrix Client-Server versions.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
requestWithPrefix: function(callback, method, path, queryParams, data, prefix) {
var fullUri = this.opts.baseUrl + prefix + path;
if (!queryParams) {
queryParams = {};
}
return this._request(callback, method, fullUri, queryParams, data);
},
_request: function(callback, method, uri, queryParams, data) {
if (callback !== undefined && !utils.isFunction(callback)) {
throw Error(
"Expected callback to be a function but got " + typeof callback
);
}
if (!queryParams) {
queryParams = {};
}
if (this.opts.extraParams) {
for (var key in this.opts.extraParams) {
if (!this.opts.extraParams.hasOwnProperty(key)) { continue; }
queryParams[key] = this.opts.extraParams[key];
}
}
var defer = q.defer();
try {
this.opts.request(
{
uri: uri,
method: method,
withCredentials: false,
qs: queryParams,
body: data,
json: true,
_matrix_opts: this.opts
},
requestCallback(defer, callback, this.opts.onlyData)
);
}
catch (ex) {
defer.reject(ex);
if (callback) {
callback(ex);
}
}
return defer.promise;
}
};
/*
* Returns a callback that can be invoked by an HTTP request on completion,
* that will either resolve or reject the given defer as well as invoke the
* given userDefinedCallback (if any).
*
* If onlyData is true, the defer/callback is invoked with the body of the
* response, otherwise the result code.
*/
var requestCallback = function(defer, userDefinedCallback, onlyData) {
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) {
defer.reject(err);
userDefinedCallback(err);
}
else {
var res = {
code: response.statusCode,
headers: response.headers,
data: body
};
defer.resolve(onlyData ? body : res);
userDefinedCallback(null, onlyData ? body : res);
}
};
};
/**
* Construct a Matrix error. This is a JavaScript Error with additional
* information specific to the standard Matrix error response.
* @constructor
* @param {Object} errorJson The Matrix error JSON returned from the homeserver.
* @prop {string} errcode The Matrix 'errcode' value, e.g. "M_FORBIDDEN".
* @prop {string} name Same as MatrixError.errcode but with a default unknown string.
* @prop {string} message The Matrix 'error' value, e.g. "Missing token."
* @prop {Object} data The raw Matrix error JSON used to construct this object.
* @prop {integer} httpStatus The numeric HTTP status code given
*/
module.exports.MatrixError = function MatrixError(errorJson) {
this.errcode = errorJson.errcode;
this.name = errorJson.errcode || "Unknown error code";
this.message = errorJson.error || "Unknown message";
this.data = errorJson;
};
module.exports.MatrixError.prototype = Object.create(Error.prototype);
/** */
module.exports.MatrixError.prototype.constructor = module.exports.MatrixError;
-173
View File
@@ -1,173 +0,0 @@
"use strict";
/**
* This is an internal module. See {@link MatrixEvent} and {@link RoomEvent} for
* the public classes.
* @module models/event
*/
/**
* Enum for event statuses.
* @readonly
* @enum {string}
*/
module.exports.EventStatus = {
/** The event was not sent and will no longer be retried. */
NOT_SENT: "not_sent",
/** The event is in the process of being sent. */
SENDING: "sending",
/** The event is in a queue waiting to be sent. */
QUEUED: "queued"
};
/**
* Construct a Matrix Event object
* @constructor
* @param {Object} event The raw event to be wrapped in this DAO
* @param {boolean} encrypted Was the event encrypted
* @prop {Object} event The raw event. <b>Do not access this property</b>
* directly unless you absolutely have to. Prefer the getter methods defined on
* this class. Using the getter methods shields your app from
* changes to event JSON between Matrix versions.
* @prop {RoomMember} sender The room member who sent this event, or null e.g.
* this is a presence event.
* @prop {RoomMember} target The room member who is the target of this event, e.g.
* the invitee, the person being banned, etc.
* @prop {EventStatus} status The sending status of the event.
* @prop {boolean} forwardLooking True if this event is 'forward looking', meaning
* that getDirectionalContent() will return event.content and not event.prev_content.
* Default: true. <strong>This property is experimental and may change.</strong>
*/
module.exports.MatrixEvent = function MatrixEvent(event, encrypted) {
this.event = event || {};
this.sender = null;
this.target = null;
this.status = null;
this.forwardLooking = true;
this.encrypted = Boolean(encrypted);
};
module.exports.MatrixEvent.prototype = {
/**
* Get the event_id for this event.
* @return {string} The event ID, e.g. <code>$143350589368169JsLZx:localhost
* </code>
*/
getId: function() {
return this.event.event_id;
},
/**
* Get the user_id for this event.
* @return {string} The user ID, e.g. <code>@alice:matrix.org</code>
*/
getSender: function() {
return this.event.user_id;
},
/**
* Get the type of event.
* @return {string} The event type, e.g. <code>m.room.message</code>
*/
getType: function() {
return this.event.type;
},
/**
* Get the type of the event that will be sent to the homeserver.
* @return {string} The event type.
*/
getWireType: function() {
return this.encryptedType || this.event.type;
},
/**
* Get the room_id for this event. This will return <code>undefined</code>
* for <code>m.presence</code> events.
* @return {string} The room ID, e.g. <code>!cURbafjkfsMDVwdRDQ:matrix.org
* </code>
*/
getRoomId: function() {
return this.event.room_id;
},
/**
* Get the timestamp of this event.
* @return {Number} The event timestamp, e.g. <code>1433502692297</code>
*/
getTs: function() {
return this.event.origin_server_ts;
},
/**
* Get the event content JSON.
* @return {Object} The event content JSON, or an empty object.
*/
getContent: function() {
return this.event.content || {};
},
/**
* Get the event content JSON that will be sent to the homeserver.
* @return {Object} The event content JSON, or an empty object.
*/
getWireContent: function() {
return this.encryptedContent || this.event.content || {};
},
/**
* Get the previous event content JSON. This will only return something for
* state events which exist in the timeline.
* @return {Object} The previous event content JSON, or an empty object.
*/
getPrevContent: function() {
return this.event.prev_content || {};
},
/**
* Get either 'content' or 'prev_content' depending on if this event is
* 'forward-looking' or not. This can be modified via event.forwardLooking.
* <strong>This method is experimental and may change.</strong>
* @return {Object} event.content if this event is forward-looking, else
* event.prev_content.
*/
getDirectionalContent: function() {
return this.forwardLooking ? this.getContent() : this.getPrevContent();
},
/**
* Get the age of this event. This represents the age of the event when the
* event arrived at the device, and not the age of the event when this
* function was called.
* @return {Number} The age of this event in milliseconds.
*/
getAge: function() {
return this.event.age;
},
/**
* Get the event state_key if it has one. This will return <code>undefined
* </code> for message events.
* @return {string} The event's <code>state_key</code>.
*/
getStateKey: function() {
return this.event.state_key;
},
/**
* Check if this event is a state event.
* @return {boolean} True if this is a state event.
*/
isState: function() {
return this.event.state_key !== undefined;
},
/**
* Check if the event is encrypted.
* @return {boolean} True if this event is encrypted.
*/
isEncrypted: function() {
return this.encrypted;
}
};
-225
View File
@@ -1,225 +0,0 @@
"use strict";
/**
* @module models/room-state
*/
var EventEmitter = require("events").EventEmitter;
var utils = require("../utils");
var RoomMember = require("./room-member");
/**
* Construct room state.
* @constructor
* @param {string} roomId Required. The ID of the room which has this state.
* @prop {Object.<string, RoomMember>} members The room member dictionary, keyed
* on the user's ID.
* @prop {Object.<string, Object.<string, MatrixEvent>>} events The state
* events dictionary, keyed on the event type and then the state_key value.
* @prop {string} paginationToken The pagination token for this state.
*/
function RoomState(roomId) {
this.roomId = roomId;
this.members = {
// userId: RoomMember
};
this.events = {
// eventType: { stateKey: MatrixEvent }
};
this.paginationToken = null;
this._sentinels = {
// userId: RoomMember
};
this._updateModifiedTime();
}
utils.inherits(RoomState, EventEmitter);
/**
* Get all RoomMembers in this room.
* @return {Array<RoomMember>} A list of RoomMembers.
*/
RoomState.prototype.getMembers = function() {
return utils.values(this.members);
};
/**
* Get a room member by their user ID.
* @param {string} userId The room member's user ID.
* @return {RoomMember} The member or null if they do not exist.
*/
RoomState.prototype.getMember = function(userId) {
return this.members[userId] || null;
};
/**
* Get a room member whose properties will not change with this room state. You
* typically want this if you want to attach a RoomMember to a MatrixEvent which
* may no longer be represented correctly by Room.currentState or Room.oldState.
* The term 'sentinel' refers to the fact that this RoomMember is an unchanging
* guardian for state at this particular point in time.
* @param {string} userId The room member's user ID.
* @return {RoomMember} The member or null if they do not exist.
*/
RoomState.prototype.getSentinelMember = function(userId) {
return this._sentinels[userId] || null;
};
/**
* Get state events from the state of the room.
* @param {string} eventType The event type of the state event.
* @param {string} stateKey Optional. The state_key of the state event. If
* this is <code>undefined</code> then all matching state events will be
* returned.
* @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was
* <code>undefined</code>, else a single event (or null if no match found).
*/
RoomState.prototype.getStateEvents = function(eventType, stateKey) {
if (!this.events[eventType]) {
// no match
return stateKey === undefined ? [] : null;
}
if (stateKey === undefined) { // return all values
return utils.values(this.events[eventType]);
}
var event = this.events[eventType][stateKey];
return event ? event : null;
};
/**
* Add an array of one or more state MatrixEvents, overwriting
* any existing state with the same {type, stateKey} tuple. Will fire
* "RoomState.events" for every event added. May fire "RoomState.members"
* if there are <code>m.room.member</code> events.
* @param {MatrixEvent[]} stateEvents a list of state events for this room.
* @fires module:client~MatrixClient#event:"RoomState.members"
* @fires module:client~MatrixClient#event:"RoomState.newMember"
* @fires module:client~MatrixClient#event:"RoomState.events"
*/
RoomState.prototype.setStateEvents = function(stateEvents) {
var self = this;
this._updateModifiedTime();
// update the core event dict
utils.forEach(stateEvents, function(event) {
if (event.getRoomId() !== self.roomId) { return; }
if (!event.isState()) { return; }
if (self.events[event.getType()] === undefined) {
self.events[event.getType()] = {};
}
self.events[event.getType()][event.getStateKey()] = event;
self.emit("RoomState.events", event, self);
});
// update higher level data structures. This needs to be done AFTER the
// core event dict as these structures may depend on other state events in
// the given array (e.g. disambiguating display names in one go to do both
// clashing names rather than progressively which only catches 1 of them).
utils.forEach(stateEvents, function(event) {
if (event.getRoomId() !== self.roomId) { return; }
if (!event.isState()) { return; }
if (event.getType() === "m.room.member") {
var userId = event.getStateKey();
var member = self.members[userId];
if (!member) {
member = new RoomMember(event.getRoomId(), userId);
self.emit("RoomState.newMember", event, self, member);
}
// Add a new sentinel for this change. We apply the same
// operations to both sentinel and member rather than deep copying
// so we don't make assumptions about the properties of RoomMember
// (e.g. and manage to break it because deep copying doesn't do
// everything).
var sentinel = new RoomMember(event.getRoomId(), userId);
utils.forEach([member, sentinel], function(roomMember) {
roomMember.setMembershipEvent(event, self);
// this member may have a power level already, so set it.
var pwrLvlEvent = self.getStateEvents("m.room.power_levels", "");
if (pwrLvlEvent) {
roomMember.setPowerLevelEvent(pwrLvlEvent);
}
});
self._sentinels[userId] = sentinel;
self.members[userId] = member;
self.emit("RoomState.members", event, self, member);
}
else if (event.getType() === "m.room.power_levels") {
var members = utils.values(self.members);
utils.forEach(members, function(member) {
member.setPowerLevelEvent(event);
});
}
});
};
/**
* Set the current typing event for this room.
* @param {MatrixEvent} event The typing event
*/
RoomState.prototype.setTypingEvent = function(event) {
utils.forEach(utils.values(this.members), function(member) {
member.setTypingEvent(event);
});
};
/**
* Update the last modified time to the current time.
*/
RoomState.prototype._updateModifiedTime = function() {
this._modified = Date.now();
};
/**
* Get the timestamp when this room state was last updated. This timestamp is
* updated when this object has received new state events.
* @return {number} The timestamp
*/
RoomState.prototype.getLastModifiedTime = function() {
return this._modified;
};
/**
* The RoomState class.
*/
module.exports = RoomState;
/**
* Fires whenever the event dictionary in room state is updated.
* @event module:client~MatrixClient#"RoomState.events"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomState} state The room state whose RoomState.events dictionary
* was updated.
* @example
* matrixClient.on("RoomState.events", function(event, state){
* var newStateEvent = event;
* });
*/
/**
* Fires whenever a member in the members dictionary is updated in any way.
* @event module:client~MatrixClient#"RoomState.members"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomState} state The room state whose RoomState.members dictionary
* was updated.
* @param {RoomMember} member The room member that was updated.
* @example
* matrixClient.on("RoomState.members", function(event, state, member){
* var newMembershipState = member.membership;
* });
*/
/**
* Fires whenever a member is added to the members dictionary. The RoomMember
* will not be fully populated yet (e.g. no membership state).
* @event module:client~MatrixClient#"RoomState.newMember"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomState} state The room state whose RoomState.members dictionary
* was updated with a new entry.
* @param {RoomMember} member The room member that was added.
* @example
* matrixClient.on("RoomState.newMember", function(event, state, member){
* // add event listeners on 'member'
* });
*/
-342
View File
@@ -1,342 +0,0 @@
"use strict";
/**
* @module models/room
*/
var EventEmitter = require("events").EventEmitter;
var RoomState = require("./room-state");
var RoomSummary = require("./room-summary");
var utils = require("../utils");
/**
* Construct a new Room.
* @constructor
* @param {string} roomId Required. The ID of this room.
* @param {*} storageToken Optional. The token which a data store can use
* to remember the state of the room. What this means is dependent on the store
* implementation.
* @prop {string} roomId The ID of this room.
* @prop {string} name The human-readable display name for this room.
* @prop {Array<MatrixEvent>} timeline The ordered list of message events for
* this room.
* @prop {RoomState} oldState The state of the room at the time of the oldest
* event in the timeline.
* @prop {RoomState} currentState The state of the room at the time of the
* newest event in the timeline.
* @prop {RoomSummary} summary The room summary.
* @prop {*} storageToken A token which a data store can use to remember
* the state of the room.
*/
function Room(roomId, storageToken) {
this.roomId = roomId;
this.name = roomId;
this.timeline = [];
this.oldState = new RoomState(roomId);
this.currentState = new RoomState(roomId);
this.summary = null;
this.storageToken = storageToken;
this._redactions = [];
}
utils.inherits(Room, EventEmitter);
/**
* Get a member from the current room state.
* @param {string} userId The user ID of the member.
* @return {RoomMember} The member or <code>null</code>.
*/
Room.prototype.getMember = function(userId) {
var member = this.currentState.members[userId];
if (!member) {
return null;
}
return member;
};
/**
* Get a list of members whose membership state is "join".
* @return {RoomMember[]} A list of currently joined members.
*/
Room.prototype.getJoinedMembers = function() {
return this.getMembersWithMemership("join");
};
/**
* Get a list of members with given membership state.
* @param {string} membership The membership state.
* @return {RoomMember[]} A list of members with the given membership state.
*/
Room.prototype.getMembersWithMemership = function(membership) {
return utils.filter(this.currentState.getMembers(), function(m) {
return m.membership === membership;
});
};
/**
* Check if the given user_id has the given membership state.
* @param {string} userId The user ID to check.
* @param {string} membership The membership e.g. <code>'join'</code>
* @return {boolean} True if this user_id has the given membership state.
*/
Room.prototype.hasMembershipState = function(userId, membership) {
return utils.filter(this.currentState.getMembers(), function(m) {
return m.membership === membership && m.userId === userId;
}).length > 0;
};
/**
* Add some events to this room's timeline. Will fire "Room.timeline" for
* each event added.
* @param {MatrixEvent[]} events A list of events to add.
* @param {boolean} toStartOfTimeline True to add these events to the start
* (oldest) instead of the end (newest) of the timeline. If true, the oldest
* event will be the <b>last</b> element of 'events'.
* @fires module:client~MatrixClient#event:"Room.timeline"
*/
Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline) {
var stateContext = toStartOfTimeline ? this.oldState : this.currentState;
function checkForRedaction(redactEvent) {
return function(e) {
return e.getId() === redactEvent.event.redacts;
};
}
for (var i = 0; i < events.length; i++) {
if (toStartOfTimeline && this._redactions.indexOf(events[i].getId()) >= 0) {
continue; // do not add the redacted event.
}
setEventMetadata(events[i], stateContext, toStartOfTimeline);
// modify state
if (events[i].isState()) {
stateContext.setStateEvents([events[i]]);
// it is possible that the act of setting the state event means we
// can set more metadata (specifically sender/target props), so try
// it again if the prop wasn't previously set.
if (!events[i].sender) {
setEventMetadata(events[i], stateContext, toStartOfTimeline);
}
}
if (events[i].getType() === "m.room.redaction") {
// try to remove the element
var removed = utils.removeElement(
this.timeline, checkForRedaction(events[i])
);
if (!removed && toStartOfTimeline) {
// redactions will trickle in BEFORE the event redacted so make
// a note of the redacted event; we'll check it later.
this._redactions.push(events[i].event.redacts);
}
// NB: We continue to add the redaction event to the timeline so clients
// can say "so and so redacted an event" if they wish to.
}
// TODO: pass through filter to see if this should be added to the timeline.
if (toStartOfTimeline) {
this.timeline.unshift(events[i]);
}
else {
this.timeline.push(events[i]);
}
this.emit("Room.timeline", events[i], this, Boolean(toStartOfTimeline));
}
};
/**
* Add some events to this room. This can include state events, message
* events and typing notifications. These events are treated as "live" so
* they will go to the end of the timeline.
* @param {MatrixEvent[]} events A list of events to add.
* @param {string} duplicateStrategy Optional. Applies to events in the
* timeline only. If this is not specified, no duplicate suppression is
* performed (this improves performance). If this is 'replace' then if a
* duplicate is encountered, the event passed to this function will replace the
* existing event in the timeline. If this is 'ignore', then the event passed to
* this function will be ignored entirely, preserving the existing event in the
* timeline. Events are identical based on their event ID <b>only</b>.
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
*/
Room.prototype.addEvents = function(events, duplicateStrategy) {
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
}
for (var i = 0; i < events.length; i++) {
if (events[i].getType() === "m.typing") {
this.currentState.setTypingEvent(events[i]);
}
else {
if (duplicateStrategy) {
// is there a duplicate?
var shouldIgnore = false;
for (var j = 0; j < this.timeline.length; j++) {
if (this.timeline[j].getId() === events[i].getId()) {
if (duplicateStrategy === "replace") {
// still need to set the right metadata on this event
setEventMetadata(
events[i],
this.currentState,
false
);
if (!this.timeline[j].encryptedType) {
this.timeline[j] = events[i];
}
// skip the insert so we don't add this event twice.
// Don't break in case we replace multiple events.
shouldIgnore = true;
}
else if (duplicateStrategy === "ignore") {
shouldIgnore = true;
break; // stop searching, we're skipping the insert
}
}
}
if (shouldIgnore) {
continue; // skip the insertion of this event.
}
}
// TODO: We should have a filter to say "only add state event
// types X Y Z to the timeline".
this.addEventsToTimeline([events[i]]);
}
}
};
/**
* Recalculate various aspects of the room, including the room name and
* room summary. Call this any time the room's current state is modified.
* May fire "Room.name" if the room name is updated.
* @param {string} userId The client's user ID.
* @fires module:client~MatrixClient#event:"Room.name"
*/
Room.prototype.recalculate = function(userId) {
var oldName = this.name;
this.name = calculateRoomName(this, userId);
this.summary = new RoomSummary(this.roomId, {
title: this.name
});
if (oldName !== this.name) {
this.emit("Room.name", this);
}
};
function setEventMetadata(event, stateContext, toStartOfTimeline) {
// set sender and target properties
event.sender = stateContext.getSentinelMember(
event.getSender()
);
if (event.getType() === "m.room.member") {
event.target = stateContext.getSentinelMember(
event.getStateKey()
);
}
if (event.isState()) {
// room state has no concept of 'old' or 'current', but we want the
// room state to regress back to previous values if toStartOfTimeline
// is set, which means inspecting prev_content if it exists. This
// is done by toggling the forwardLooking flag.
if (toStartOfTimeline) {
event.forwardLooking = false;
}
}
}
/**
* This is an internal method. Calculates the name of the room from the current
* room state.
* @param {Room} room The matrix room.
* @param {string} userId The client's user ID. Used to filter room members
* correctly.
* @return {string} The calculated room name.
*/
function calculateRoomName(room, userId) {
// check for an alias, if any. for now, assume first alias is the
// official one.
var alias;
var mRoomAliases = room.currentState.getStateEvents("m.room.aliases")[0];
if (mRoomAliases && utils.isArray(mRoomAliases.getContent().aliases)) {
alias = mRoomAliases.getContent().aliases[0];
}
var mRoomName = room.currentState.getStateEvents('m.room.name', '');
if (mRoomName) {
return mRoomName.getContent().name + (false && alias ? " (" + alias + ")" : "");
}
else if (alias) {
return alias;
}
else {
// get members that are NOT ourselves and are actually in the room.
var members = utils.filter(room.currentState.getMembers(), function(m) {
return (m.userId !== userId && m.membership !== "leave");
});
// TODO: Localisation
if (members.length === 0) {
var memberList = utils.filter(room.currentState.getMembers(), function(m) {
return (m.membership !== "leave");
});
if (memberList.length === 1) {
// we exist, but no one else... self-chat or invite.
if (memberList[0].membership === "invite") {
if (memberList[0].events.member) {
// extract who invited us to the room
return "Invite from " + memberList[0].events.member.getSender();
}
else {
return "Room Invite";
}
}
else {
return userId;
}
}
else {
// there really isn't anyone in this room...
return "?";
}
}
else if (members.length === 1) {
return members[0].name;
}
else if (members.length === 2) {
return (
members[0].name + " and " + members[1].name
);
}
else {
return (
members[0].name + " and " + (members.length - 1) + " others"
);
}
}
}
/**
* The Room class.
*/
module.exports = Room;
/**
* Fires whenever the timeline in a room is updated.
* @event module:client~MatrixClient#"Room.timeline"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {Room} room The room whose Room.timeline was updated.
* @param {boolean} toStartOfTimeline True if this event was added to the start
* (beginning; oldest) of the timeline e.g. due to pagination.
* @example
* matrixClient.on("Room.timeline", function(event, room, toStartOfTimeline){
* if (toStartOfTimeline) {
* var messageToAppend = room.timeline[room.timeline.length - 1];
* }
* });
*/
/**
* Fires whenever the name of a room is updated.
* @event module:client~MatrixClient#"Room.name"
* @param {Room} room The room whose Room.name was updated.
* @example
* matrixClient.on("Room.name", function(room){
* var newName = room.name;
* });
*/
-130
View File
@@ -1,130 +0,0 @@
"use strict";
/**
* @module models/user
*/
var EventEmitter = require("events").EventEmitter;
var utils = require("../utils");
/**
* Construct a new User. A User must have an ID and can optionally have extra
* information associated with it.
* @constructor
* @param {string} userId Required. The ID of this user.
* @prop {string} userId The ID of the user.
* @prop {Object} info The info object supplied in the constructor.
* @prop {string} displayName The 'displayname' of the user if known.
* @prop {string} avatarUrl The 'avatar_url' of the user if known.
* @prop {string} presence The presence enum if known.
* @prop {Number} lastActiveAgo The last time the user performed some action in ms.
* @prop {Object} events The events describing this user.
* @prop {MatrixEvent} events.presence The m.presence event for this user.
*/
function User(userId) {
this.userId = userId;
this.presence = "offline";
this.displayName = userId;
this.avatarUrl = null;
this.lastActiveAgo = 0;
this.events = {
presence: null,
profile: null
};
this._updateModifiedTime();
}
utils.inherits(User, EventEmitter);
/**
* Update this User with the given presence event. May fire "User.presence",
* "User.avatarUrl" and/or "User.displayName" if this event updates this user's
* properties.
* @param {MatrixEvent} event The <code>m.presence</code> event.
* @fires module:client~MatrixClient#event:"User.presence"
* @fires module:client~MatrixClient#event:"User.displayName"
* @fires module:client~MatrixClient#event:"User.avatarUrl"
*/
User.prototype.setPresenceEvent = function(event) {
if (event.getType() !== "m.presence") {
return;
}
var firstFire = this.events.presence === null;
this.events.presence = event;
var eventsToFire = [];
if (event.getContent().presence !== this.presence || firstFire) {
eventsToFire.push("User.presence");
}
if (event.getContent().avatar_url !== this.avatarUrl) {
eventsToFire.push("User.avatarUrl");
}
if (event.getContent().displayname !== this.displayName) {
eventsToFire.push("User.displayName");
}
this.presence = event.getContent().presence;
this.displayName = event.getContent().displayname;
this.avatarUrl = event.getContent().avatar_url;
this.lastActiveAgo = event.getContent().last_active_ago;
if (eventsToFire.length > 0) {
this._updateModifiedTime();
}
for (var i = 0; i < eventsToFire.length; i++) {
this.emit(eventsToFire[i], event, this);
}
};
/**
* Update the last modified time to the current time.
*/
User.prototype._updateModifiedTime = function() {
this._modified = Date.now();
};
/**
* Get the timestamp when this User was last updated. This timestamp is
* updated when this User receives a new Presence event which has updated a
* property on this object. It is updated <i>before</i> firing events.
* @return {number} The timestamp
*/
User.prototype.getLastModifiedTime = function() {
return this._modified;
};
/**
* The User class.
*/
module.exports = User;
/**
* Fires whenever any user's presence changes.
* @event module:client~MatrixClient#"User.presence"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {User} user The user whose User.presence changed.
* @example
* matrixClient.on("User.presence", function(event, user){
* var newPresence = user.presence;
* });
*/
/**
* Fires whenever any user's display name changes.
* @event module:client~MatrixClient#"User.displayName"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {User} user The user whose User.displayName changed.
* @example
* matrixClient.on("User.displayName", function(event, user){
* var newName = user.displayName;
* });
*/
/**
* Fires whenever any user's avatar URL changes.
* @event module:client~MatrixClient#"User.avatarUrl"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {User} user The user whose User.avatarUrl changed.
* @example
* matrixClient.on("User.avatarUrl", function(event, user){
* var newUrl = user.avatarUrl;
* });
*/
-259
View File
@@ -1,259 +0,0 @@
/**
* @module pushprocessor
*/
/**
* Construct a Push Processor.
* @constructor
* @param {Object} client The Matrix client object to use
*/
function PushProcessor(client) {
var escapeRegExp = function(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
var matchingRuleFromKindSet = function(ev, kindset, device) {
var rulekinds_in_order = ['override', 'content', 'room', 'sender', 'underride'];
for (var ruleKindIndex = 0;
ruleKindIndex < rulekinds_in_order.length;
++ruleKindIndex) {
var kind = rulekinds_in_order[ruleKindIndex];
var ruleset = kindset[kind];
for (var ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) {
var rule = ruleset[ruleIndex];
if (!rule.enabled) { continue; }
var rawrule = templateRuleToRaw(kind, rule, device);
if (!rawrule) { continue; }
if (ruleMatchesEvent(rawrule, ev)) {
rule.kind = kind;
return rule;
}
}
}
return null;
};
var templateRuleToRaw = function(kind, tprule, device) {
var rawrule = {
'rule_id': tprule.rule_id,
'actions': tprule.actions,
'conditions': []
};
switch (kind) {
case 'underride':
case 'override':
rawrule.conditions = tprule.conditions;
break;
case 'room':
if (!tprule.rule_id) { return null; }
rawrule.conditions.push({
'kind': 'event_match',
'key': 'room_id',
'pattern': tprule.rule_id
});
break;
case 'sender':
if (!tprule.rule_id) { return null; }
rawrule.conditions.push({
'kind': 'event_match',
'key': 'user_id',
'pattern': tprule.rule_id
});
break;
case 'content':
if (!tprule.pattern) { return null; }
rawrule.conditions.push({
'kind': 'event_match',
'key': 'content.body',
'pattern': tprule.pattern
});
break;
}
if (device) {
rawrule.conditions.push({
'kind': 'device',
'profile_tag': device
});
}
return rawrule;
};
var ruleMatchesEvent = function(rule, ev) {
var ret = true;
for (var i = 0; i < rule.conditions.length; ++i) {
var cond = rule.conditions[i];
ret &= eventFulfillsCondition(cond, ev);
}
//console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match"));
return ret;
};
var eventFulfillsCondition = function(cond, ev) {
var condition_functions = {
"event_match": eventFulfillsEventMatchCondition,
"device": eventFulfillsDeviceCondition,
"contains_display_name": eventFulfillsDisplayNameCondition,
"room_member_count": eventFulfillsRoomMemberCountCondition
};
if (condition_functions[cond.kind]) {
return condition_functions[cond.kind](cond, ev);
}
return true;
};
var eventFulfillsRoomMemberCountCondition = function(cond, ev) {
if (!cond.is) { return false; }
var room = client.getRoom(ev.room_id);
if (!room || !room.currentState || !room.currentState.members) { return false; }
var memberCount = Object.keys(room.currentState.members).length;
var m = cond.is.match(/^([=<>]*)([0-9]*)$/);
if (!m) { return false; }
var ineq = m[1];
var rhs = parseInt(m[2]);
if (isNaN(rhs)) { return false; }
switch (ineq) {
case '':
case '==':
return memberCount == rhs;
case '<':
return memberCount < rhs;
case '>':
return memberCount > rhs;
case '<=':
return memberCount <= rhs;
case '>=':
return memberCount >= rhs;
default:
return false;
}
};
var eventFulfillsDisplayNameCondition = function(cond, ev) {
if (!ev.content || ! ev.content.body || typeof ev.content.body != 'string') {
return false;
}
var room = client.getRoom(ev.room_id);
if (!room || !room.currentState || !room.currentState.members ||
!room.currentState.getMember(client.credentials.userId)) { return false; }
var displayName = room.currentState.getMember(client.credentials.userId).name;
var pat = new RegExp("\\b" + escapeRegExp(displayName) + "\\b", 'i');
return ev.content.body.search(pat) > -1;
};
var eventFulfillsDeviceCondition = function(cond, ev) {
return false; // XXX: Allow a profile tag to be set for the web client instance
};
var eventFulfillsEventMatchCondition = function(cond, ev) {
var val = valueForDottedKey(cond.key, ev);
if (!val || typeof val != 'string') { return false; }
var pat;
if (cond.key == 'content.body') {
pat = '\\b' + globToRegexp(cond.pattern) + '\\b';
} else {
pat = '^' + globToRegexp(cond.pattern) + '$';
}
var regex = new RegExp(pat, 'i');
return !!val.match(regex);
};
var globToRegexp = function(glob) {
// From
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
// Because micromatch is about 130KB with dependencies,
// and minimatch is not much better.
var pat = escapeRegExp(glob);
pat = pat.replace(/\\\*/, '.*');
pat = pat.replace(/\?/, '.');
pat = pat.replace(/\\\[(!|)(.*)\\]/, function(match, p1, p2, offset, string) {
var first = p1 && '^' || '';
var second = p2.replace(/\\\-/, '-');
return '[' + first + second + ']';
});
return pat;
};
var valueForDottedKey = function(key, ev) {
var parts = key.split('.');
var val = ev;
while (parts.length > 0) {
var thispart = parts.shift();
if (!val[thispart]) { return null; }
val = val[thispart];
}
return val;
};
var matchingRuleForEventWithRulesets = function(ev, rulesets) {
if (!rulesets) { return null; }
if (ev.user_id == client.credentials.userId) { return null; }
var allDevNames = Object.keys(rulesets.device);
for (var i = 0; i < allDevNames.length; ++i) {
var devname = allDevNames[i];
var devrules = rulesets.device[devname];
var matchingRule = matchingRuleFromKindSet(devrules, devname);
if (matchingRule) { return matchingRule; }
}
return matchingRuleFromKindSet(ev, rulesets.global);
};
var actionListToActionsObject = function(actionlist) {
var actionobj = { 'notify': false, 'tweaks': {} };
for (var i = 0; i < actionlist.length; ++i) {
var action = actionlist[i];
if (action === 'notify') {
actionobj.notify = true;
} else if (typeof action === 'object') {
if (action.value === undefined) { action.value = true; }
actionobj.tweaks[action.set_tweak] = action.value;
}
}
return actionobj;
};
var pushActionsForEventAndRulesets = function(ev, rulesets) {
var rule = matchingRuleForEventWithRulesets(ev, rulesets);
if (!rule) { return {}; }
var actionObj = actionListToActionsObject(rule.actions);
// Some actions are implicit in some situations: we add those here
if (actionObj.tweaks.highlight === undefined) {
// if it isn't specified, highlight if it's a content
// rule but otherwise not
actionObj.tweaks.highlight = (rule.kind == 'content');
}
return actionObj;
};
this.actionsForEvent = function(ev) {
return pushActionsForEventAndRulesets(ev, client.pushRules);
};
}
/**
* @typedef {Object} PushAction
* @type {Object}
* @property {boolean} notify Whether this event should notify the user or not.
* @property {Object} tweaks How this event should be notified.
* @property {boolean} tweaks.highlight Whether this event should be highlighted
* on the UI.
* @property {boolean} tweaks.sound Whether this notification should produce a
* noise.
*/
/** The PushProcessor class. */
module.exports = PushProcessor;
-119
View File
@@ -1,119 +0,0 @@
"use strict";
/**
* This is an internal module. See {@link MatrixInMemoryStore} for the public class.
* @module store/memory
*/
var utils = require("../utils");
/**
* Construct a new in-memory data store for the Matrix Client.
* @constructor
*/
module.exports.MatrixInMemoryStore = function MatrixInMemoryStore() {
this.rooms = {
// roomId: Room
};
this.users = {
// userId: User
};
this.syncToken = null;
};
module.exports.MatrixInMemoryStore.prototype = {
/**
* Retrieve the token to stream from.
* @return {string} The token or null.
*/
getSyncToken: function() {
return this.syncToken;
},
/**
* Set the token to stream from.
* @param {string} token The token to stream from.
*/
setSyncToken: function(token) {
this.syncToken = token;
},
/**
* Store the given room.
* @param {Room} room The room to be stored. All properties must be stored.
*/
storeRoom: function(room) {
this.rooms[room.roomId] = room;
},
/**
* Retrieve a room by its' room ID.
* @param {string} roomId The room ID.
* @return {Room} The room or null.
*/
getRoom: function(roomId) {
return this.rooms[roomId] || null;
},
/**
* Retrieve all known rooms.
* @return {Room[]} A list of rooms, which may be empty.
*/
getRooms: function() {
return utils.values(this.rooms);
},
/**
* Retrieve a summary of all the rooms.
* @return {RoomSummary[]} A summary of each room.
*/
getRoomSummaries: function() {
return utils.map(utils.values(this.rooms), function(room) {
return room.summary;
});
},
/**
* Store a User.
* @param {User} user The user to store.
*/
storeUser: function(user) {
this.users[user.userId] = user;
},
/**
* Retrieve a User by its' user ID.
* @param {string} userId The user ID.
* @return {User} The user or null.
*/
getUser: function(userId) {
return this.users[userId] || null;
},
/**
* Retrieve scrollback for this room.
* @param {Room} room The matrix room
* @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.
*/
scrollback: function(room, limit) {
return [];
},
/**
* 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.
*/
storeEvents: function(room, events, token, toStart) {
// no-op because they've already been added to the room instance.
}
// TODO
//setMaxHistoryPerRoom: function(maxHistory) {},
// TODO
//reapOldMessages: function() {},
};
-147
View File
@@ -1,147 +0,0 @@
"use strict";
/**
* @module store/session/webstorage
*/
var utils = require("../../utils");
var DEBUG = false; // set true to enable console logging.
var E2E_PREFIX = "session.e2e.";
/**
* Construct a web storage session store, capable of storing account keys,
* session keys and access tokens.
* @constructor
* @param {WebStorage} webStore A web storage implementation, e.g.
* 'window.localStorage' or 'window.sessionStorage' or a custom implementation.
* @throws if the supplied 'store' does not meet the Storage interface of the
* WebStorage API.
*/
function WebStorageSessionStore(webStore) {
this.store = webStore;
if (!utils.isFunction(webStore.getItem) ||
!utils.isFunction(webStore.setItem) ||
!utils.isFunction(webStore.removeItem)) {
throw new Error(
"Supplied webStore does not meet the WebStorage API interface"
);
}
}
WebStorageSessionStore.prototype = {
/**
* Store the end to end account for the logged-in user.
* @param {string} account Base64 encoded account.
*/
storeEndToEndAccount: function(account) {
this.store.setItem(KEY_END_TO_END_ACCOUNT, account);
},
/**
* Load the end to end account for the logged-in user.
* @return {?string} Base64 encoded account.
*/
getEndToEndAccount: function() {
return this.store.getItem(KEY_END_TO_END_ACCOUNT);
},
/**
* Stores the known devices for a user.
* @param {string} userId The user's ID.
* @param {object} devices A map from device ID to keys for the device.
*/
storeEndToEndDevicesForUser: function(userId, devices) {
setJsonItem(this.store, keyEndToEndDevicesForUser(userId), devices);
},
/**
* Retrieves the known devices for a user.
* @param {string} userId The user's ID.
* @return {object} A map from device ID to keys for the device.
*/
getEndToEndDevicesForUser: function(userId) {
return getJsonItem(this.store, keyEndToEndDevicesForUser(userId));
},
/**
* Store a session between the logged-in user and another device
* @param {string} deviceKey The public key of the other device.
* @param {string} sessionId The ID for this end-to-end session.
* @param {string} session Base64 encoded end-to-end session.
*/
storeEndToEndSession: function(deviceKey, sessionId, session) {
var sessions = this.getEndToEndSessions(deviceKey) || {};
sessions[sessionId] = session;
setJsonItem(
this.store, keyEndToEndSessions(deviceKey), sessions
);
},
/**
* Retrieve the end-to-end sessions between the logged-in user and another
* device.
* @param {string} deviceKey The public key of the other device.
* @return {object} A map from sessionId to Base64 end-to-end session.
*/
getEndToEndSessions: function(deviceKey) {
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
},
/**
* Store the end-to-end state for a room.
* @param {string} roomId The room's ID.
* @param {object} roomInfo The end-to-end info for the room.
*/
storeEndToEndRoom: function(roomId, roomInfo) {
setJsonItem(this.store, keyEndToEndRoom(roomId), roomInfo);
},
/**
* Get the end-to-end state for a room
* @param {string} roomId The room's ID.
* @return {object} The end-to-end info for the room.
*/
getEndToEndRoom: function(roomId) {
return getJsonItem(this.store, keyEndToEndRoom(roomId));
}
};
var KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
function keyEndToEndDevicesForUser(userId) {
return E2E_PREFIX + "devices/" + userId;
}
function keyEndToEndSessions(deviceKey) {
return E2E_PREFIX + "sessions/" + deviceKey;
}
function keyEndToEndRoom(roomId) {
return E2E_PREFIX + "rooms/" + roomId;
}
function getJsonItem(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 setJsonItem(store, key, val) {
store.setItem(key, JSON.stringify(val));
}
function debuglog() {
if (DEBUG) {
console.log.apply(console, arguments);
}
}
/** */
module.exports = WebStorageSessionStore;
-651
View File
@@ -1,651 +0,0 @@
"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);
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"), []);
};
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, 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.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;
-321
View File
@@ -1,321 +0,0 @@
"use strict";
/**
* This is an internal module.
* @module utils
*/
/**
* Encode a dictionary of query parameters.
* @param {Object} params A dict of key/values to encode e.g.
* {"foo": "bar", "baz": "taz"}
* @return {string} The encoded string e.g. foo=bar&baz=taz
*/
module.exports.encodeParams = function(params) {
var qs = "";
for (var key in params) {
if (!params.hasOwnProperty(key)) { continue; }
qs += "&" + encodeURIComponent(key) + "=" +
encodeURIComponent(params[key]);
}
return qs.substring(1);
};
/**
* Encodes a URI according to a set of template variables. Variables will be
* passed through encodeURIComponent.
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
* @param {Object} variables The key/value pairs to replace the template
* variables with. E.g. { "$bar": "baz" }.
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
*/
module.exports.encodeUri = function(pathTemplate, variables) {
for (var key in variables) {
if (!variables.hasOwnProperty(key)) { continue; }
pathTemplate = pathTemplate.replace(
key, encodeURIComponent(variables[key])
);
}
return pathTemplate;
};
/**
* Applies a map function to the given array.
* @param {Array} array The array to apply the function to.
* @param {Function} fn The function that will be invoked for each element in
* the array with the signature <code>fn(element){...}</code>
* @return {Array} A new array with the results of the function.
*/
module.exports.map = function(array, fn) {
var results = new Array(array.length);
for (var i = 0; i < array.length; i++) {
results[i] = fn(array[i]);
}
return results;
};
/**
* Applies a filter function to the given array.
* @param {Array} array The array to apply the function to.
* @param {Function} fn The function that will be invoked for each element in
* the array. It should return true to keep the element. The function signature
* looks like <code>fn(element, index, array){...}</code>.
* @return {Array} A new array with the results of the function.
*/
module.exports.filter = function(array, fn) {
var results = [];
for (var i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
results.push(array[i]);
}
}
return results;
};
/**
* Get the keys for an object. Same as <code>Object.keys()</code>.
* @param {Object} obj The object to get the keys for.
* @return {string[]} The keys of the object.
*/
module.exports.keys = function(obj) {
var keys = [];
for (var key in obj) {
if (!obj.hasOwnProperty(key)) { continue; }
keys.push(key);
}
return keys;
};
/**
* Get the values for an object.
* @param {Object} obj The object to get the values for.
* @return {Array<*>} The values of the object.
*/
module.exports.values = function(obj) {
var values = [];
for (var key in obj) {
if (!obj.hasOwnProperty(key)) { continue; }
values.push(obj[key]);
}
return values;
};
/**
* Invoke a function for each item in the array.
* @param {Array} array The array.
* @param {Function} fn The function to invoke for each element. Has the
* function signature <code>fn(element, index)</code>.
*/
module.exports.forEach = function(array, fn) {
for (var i = 0; i < array.length; i++) {
fn(array[i], i);
}
};
/**
* The findElement() method returns a value in the array, if an element in the array
* satisfies (returns true) the provided testing function. Otherwise undefined
* is returned.
* @param {Array} array The array.
* @param {Function} fn Function to execute on each value in the array, with the
* function signature <code>fn(element, index, array)</code>
* @param {boolean} reverse True to search in reverse order.
* @return {*} The first value in the array which returns <code>true</code> for
* the given function.
*/
module.exports.findElement = function(array, fn, reverse) {
var i;
if (reverse) {
for (i = array.length - 1; i >= 0; i--) {
if (fn(array[i], i, array)) {
return array[i];
}
}
}
else {
for (i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
return array[i];
}
}
}
};
/**
* The removeElement() method removes the first element in the array that
* satisfies (returns true) the provided testing function.
* @param {Array} array The array.
* @param {Function} fn Function to execute on each value in the array, with the
* function signature <code>fn(element, index, array)</code>. Return true to
* remove this element and break.
* @param {boolean} reverse True to search in reverse order.
* @return {boolean} True if an element was removed.
*/
module.exports.removeElement = function(array, fn, reverse) {
var i;
if (reverse) {
for (i = array.length - 1; i >= 0; i--) {
if (fn(array[i], i, array)) {
array.splice(i, 1);
return true;
}
}
}
else {
for (i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
array.splice(i, 1);
return true;
}
}
}
return false;
};
/**
* Checks if the given thing is a function.
* @param {*} value The thing to check.
* @return {boolean} True if it is a function.
*/
module.exports.isFunction = function(value) {
return Object.prototype.toString.call(value) == "[object Function]";
};
/**
* Checks if the given thing is an array.
* @param {*} value The thing to check.
* @return {boolean} True if it is an array.
*/
module.exports.isArray = function(value) {
return Boolean(value && value.constructor === Array);
};
/**
* Checks that the given object has the specified keys.
* @param {Object} obj The object to check.
* @param {string[]} keys The list of keys that 'obj' must have.
* @throws If the object is missing keys.
*/
module.exports.checkObjectHasKeys = function(obj, keys) {
for (var i = 0; i < keys.length; i++) {
if (!obj.hasOwnProperty(keys[i])) {
throw new Error("Missing required key: " + keys[i]);
}
}
};
/**
* Checks that the given object has no extra keys other than the specified ones.
* @param {Object} obj The object to check.
* @param {string[]} allowedKeys The list of allowed key names.
* @throws If there are extra keys.
*/
module.exports.checkObjectHasNoAdditionalKeys = function(obj, allowedKeys) {
for (var key in obj) {
if (!obj.hasOwnProperty(key)) { continue; }
if (allowedKeys.indexOf(key) === -1) {
throw new Error("Unknown key: " + key);
}
}
};
/**
* Deep copy the given object. The object MUST NOT have circular references and
* MUST NOT have functions.
* @param {Object} obj The object to deep copy.
* @return {Object} A copy of the object without any references to the original.
*/
module.exports.deepCopy = function(obj) {
return JSON.parse(JSON.stringify(obj));
};
/**
* Inherit the prototype methods from one constructor into another. This is a
* port of the Node.js implementation with an Object.create polyfill.
*
* @param {function} ctor Constructor function which needs to inherit the
* prototype.
* @param {function} superCtor Constructor function to inherit prototype from.
*/
module.exports.inherits = function(ctor, superCtor) {
// Add Object.create polyfill for IE8
// Source:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript
// /Reference/Global_Objects/Object/create#Polyfill
if (typeof Object.create != 'function') {
// Production steps of ECMA-262, Edition 5, 15.2.3.5
// Reference: http://es5.github.io/#x15.2.3.5
Object.create = (function() {
// To save on memory, use a shared constructor
function Temp() {}
// make a safe reference to Object.prototype.hasOwnProperty
var hasOwn = Object.prototype.hasOwnProperty;
return function(O) {
// 1. If Type(O) is not Object or Null throw a TypeError exception.
if (typeof O != 'object') {
throw new TypeError('Object prototype may only be an Object or null');
}
// 2. Let obj be the result of creating a new object as if by the
// expression new Object() where Object is the standard built-in
// constructor with that name
// 3. Set the [[Prototype]] internal property of obj to O.
Temp.prototype = O;
var obj = new Temp();
Temp.prototype = null; // Let's not keep a stray reference to O...
// 4. If the argument Properties is present and not undefined, add
// own properties to obj as if by calling the standard built-in
// function Object.defineProperties with arguments obj and
// Properties.
if (arguments.length > 1) {
// Object.defineProperties does ToObject on its first argument.
var Properties = Object(arguments[1]);
for (var prop in Properties) {
if (hasOwn.call(Properties, prop)) {
obj[prop] = Properties[prop];
}
}
}
// 5. Return obj
return obj;
};
})();
}
// END polyfill
// Add util.inherits from Node.js
// Source:
// https://github.com/joyent/node/blob/master/lib/util.js
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
ctor.super_ = superCtor;
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
};
+58 -11
View File
@@ -1,16 +1,19 @@
{
"name": "matrix-js-sdk",
"version": "0.2.2",
"version": "0.7.5",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"test": "istanbul cover --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" jasmine-node -- spec --verbose --junitreport --forceexit --captureExceptions",
"check": "jasmine-node spec --verbose --junitreport --forceexit --captureExceptions",
"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 --max_line_length 90 -r spec/ -r lib/",
"release": "npm run build && mkdir dist/$npm_package_version && uglifyjs -c -m -o dist/$npm_package_version/browser-matrix-$npm_package_version.min.js dist/browser-matrix-dev.js && cp dist/browser-matrix-dev.js dist/$npm_package_version/browser-matrix-$npm_package_version.js"
"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"
@@ -20,17 +23,61 @@
],
"browser": "browser-index.js",
"author": "matrix.org",
"license": "Apache 2.0",
"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",
"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-2.2.1.tgz"
},
"browserify": {
"transform": [
"sourceify",
"browserify-shim"
]
},
"browserify-shim": {
"olm": "global:Olm"
}
}
Executable
+247
View File
@@ -0,0 +1,247 @@
#!/bin/bash
#
# Script to perform a release of matrix-js-sdk.
#
# Requires:
# github-changelog-generator; install via:
# pip install git+https://github.com/matrix-org/github-changelog-generator.git
# 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() {
cat <<EOF
$USAGE
-c changelog_file: specify name of file containing changelog
-x: skip updating the changelog
-z: skip generating the jsdoc
EOF
}
ret=0
cat package.json | jq '.dependencies[]' | grep -q '#develop' || ret=$?
if [ "$ret" -eq 0 ]; then
echo "package.json contains develop dependencies. Refusing to release."
exit
fi
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"
while getopts hc:xz f; do
case $f in
h)
help
exit 0
;;
c)
changelog_file="$OPTARG"
;;
x)
skip_changelog=1
;;
z)
skip_jsdoc=1
;;
esac
done
shift `expr $OPTIND - 1`
if [ $# -ne 1 ]; then
echo "Usage: $USAGE" >&2
exit 1
fi
if [ -z "$skip_changelog" ]; then
# update_changelog doesn't have a --version flag
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
fi
# ignore leading v on release
release="${1#v}"
tag="v${release}"
rel_branch="release-$tag"
prerelease=0
# We check if this build is a prerelease by looking to
# see if the version has a hyphen in it. Crude,
# but semver doesn't support postreleases so anything
# with a hyphen is a prerelease.
echo $release | grep -q '-' && prerelease=1
if [ $prerelease -eq 1 ]; then
echo Making a PRE-RELEASE
fi
if [ -z "$skip_changelog" ]; then
if ! command -v update_changelog >/dev/null 2>&1; then
echo "release.sh requires github-changelog-generator. Try:" >&2
echo " pip install git+https://github.com/matrix-org/github-changelog-generator.git" >&2
exit 1
fi
fi
# we might already be on the release branch, in which case, yay
# If we're on any branch starting with 'release', we don't create
# a separate release branch (this allows us to use the same
# release branch for releases and release candidates).
curbranch=$(git symbolic-ref --short HEAD)
if [[ "$curbranch" != release* ]]; then
echo "Creating release branch"
git checkout -b "$rel_branch"
else
echo "Using current branch ($curbranch) for release"
rel_branch=$curbranch
fi
if [ -z "$skip_changelog" ]; then
echo "Generating changelog"
update_changelog -f "$changelog_file" "$release"
read -p "Edit $changelog_file manually, or press enter to continue " REPLY
if [ -n "$(git ls-files --modified $changelog_file)" ]; then
echo "Committing updated changelog"
git commit "$changelog_file" -m "Prepare changelog for $tag"
fi
fi
latest_changes=`mktemp`
cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_changes}"
set -x
# Bump package.json and build the dist
echo "npm version"
# npm version will automatically commit its modification
# and make a release tag. We don't want it to create the tag
# because it can only sign with the default key, but we can
# only turn off both of these behaviours, so we have to
# manually commit the result.
npm version --no-git-tag-version "$release"
git commit package.json -m "$tag"
# figure out if we should be signing this release
signing_id=
if [ -f release_config.yaml ]; then
signing_id=`cat release_config.yaml | python -c "import yaml; import sys; print yaml.load(sys.stdin)['signing_id']"`
fi
# If there is a 'dist' script in the package.json,
# run it in a separate checkout of the project, then
# upload any files in the 'dist' directory as release
# assets.
# We make a completely separate checkout to be sure
# we're using released versions of the dependencies
# (rather than whatever we're pulling in from npm link)
assets=''
dodist=0
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
if [ $dodist -eq 0 ]; then
projdir=`pwd`
builddir=`mktemp -d 2>/dev/null || mktemp -d -t 'mytmpdir'`
echo "Building distribution copy in $builddir"
pushd "$builddir"
git clone "$projdir" .
git checkout "$rel_branch"
npm install
# We haven't tagged yet, so tell the dist script what version
# it's building
DIST_VERSION="$tag" npm run dist
popd
for i in "$builddir"/dist/*; do
assets="$assets -a $i"
if [ -n "$signing_id" ]
then
gpg -u "$signing_id" --armor --output "$i".asc --detach-sig "$i"
assets="$assets -a $i.asc"
fi
done
fi
# push the release branch (github can't release from
# a branch it doesn't have)
git push origin "$rel_branch"
if [ -n "$signing_id" ]; then
# make a signed tag
# gnupg seems to fail to get the right tty device unless we set it here
GIT_COMMITTER_EMAIL="$signing_id" GPG_TTY=`tty` git tag -u "$signing_id" -F "${latest_changes}" "$tag"
else
git tag -a -F "${latest_changes}" "$tag"
fi
# push the tag
git push origin "$tag"
hubflags=''
if [ $prerelease -eq 1 ]; then
hubflags='-p'
fi
release_text=`mktemp`
echo "$tag" > "${release_text}"
echo >> "${release_text}"
cat "${latest_changes}" >> "${release_text}"
hub release create $hubflags $assets -f "${release_text}" "$tag"
if [ $dodist -eq 0 ]; then
rm -rf "$builddir"
fi
rm "${release_text}"
rm "${latest_changes}"
if [ -z "$skip_jsdoc" ]; then
echo "generating jsdocs"
npm run gendoc
echo "copying jsdocs to gh-pages branch"
git checkout gh-pages
git pull
cp -a ".jsdoc/matrix-js-sdk/$release" .
perl -i -pe 'BEGIN {$rel=shift} $_ =~ /^<\/ul>/ && print
"<li><a href=\"${rel}/index.html\">Version ${rel}</a></li>\n"' \
$release index.html
git add "$release"
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
fi
# merge release branch to master
echo "updating master branch"
git checkout master
git pull
git merge --ff-only "$rel_branch"
# push master and docs (if generated) to github
git push origin master
if [ -z "$skip_jsdoc" ]; then
git push origin gh-pages
fi
# publish to npmjs
npm publish
# finally, merge master back onto develop
git checkout develop
git pull
git merge master
git push origin develop
+18
View File
@@ -0,0 +1,18 @@
#!/usr/bin/env python
"""
Outputs the body of the first entry of changelog file on stdin
"""
import re
import sys
found_first_header = False
for line in sys.stdin:
line = line.strip()
if re.match(r"^Changes in \[.*\]", line):
if found_first_header:
break
found_first_header = True
elif not re.match(r"^=+$", line) and len(line) > 0:
print line
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
env: {
jasmine: true,
},
}
+56
View File
@@ -0,0 +1,56 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* A mock implementation of the webstorage api
* @constructor
*/
function MockStorageApi() {
this.data = {};
this.keys = [];
this.length = 0;
}
MockStorageApi.prototype = {
setItem: function(k, v) {
this.data[k] = v;
this._recalc();
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
this._recalc();
},
key: function(index) {
return this.keys[index];
},
_recalc: function() {
const keys = [];
for (const k in this.data) {
if (!this.data.hasOwnProperty(k)) {
continue;
}
keys.push(k);
}
this.keys = keys;
this.length = keys.length;
},
};
/** */
module.exports = MockStorageApi;
+741 -197
View File
@@ -1,244 +1,788 @@
"use strict";
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var utils = require("../test-utils");
function MockStorageApi() {
this.data = {};
const sdk = require("../..");
const q = require("q");
const HttpBackend = require("../mock-request");
const utils = require("../../lib/utils");
const testUtils = require("../test-utils");
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;
/**
* Set an expectation that the client will upload device keys and a number of
* one-time keys; then flush the http requests.
*
* @param {string} deviceId expected device id in upload request
* @param {object} httpBackend
*
* @return {promise} completes once the http requests have completed, returning combined
* {one_time_keys: {}, device_keys: {}}
*/
function expectKeyUpload(deviceId, httpBackend) {
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: {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({});
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: {signed_curve25519: count}};
});
return httpBackend.flush(uploadPath, 2).then(function() {
return keys;
});
}
MockStorageApi.prototype = {
setItem: function(k, v) {
this.data[k] = v;
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
}
};
/**
* Set an expectation that ali will upload device keys and a number of one-time keys;
* then flush the http requests.
*
* <p>Updates <tt>aliDeviceKeys</tt>
*
* @return {promise} completes once the http requests have completed.
*/
function expectAliKeyUpload() {
return expectKeyUpload(aliDeviceId, aliHttpBackend).then(function(content) {
aliDeviceKeys = content.device_keys;
});
}
/**
* Set an expectation that bob will upload device keys and a number of one-time keys;
* then flush the http requests.
*
* <p>Updates <tt>bobDeviceKeys</tt>, <tt>bobOneTimeKeys</tt>,
* <tt>bobDeviceCurve25519Key</tt>, <tt>bobDeviceEd25519Key</tt>
*
* @return {promise} completes once the http requests have completed.
*/
function expectBobKeyUpload() {
return expectKeyUpload(bobDeviceId, bobHttpBackend).then(function(content) {
bobDeviceKeys = content.device_keys;
bobOneTimeKeys = content.one_time_keys;
expect(bobDeviceKeys).toBeDefined();
expect(bobOneTimeKeys).toBeDefined();
bobDeviceCurve25519Key = bobDeviceKeys.keys["curve25519:bvcxz"];
bobDeviceEd25519Key = bobDeviceKeys.keys["ed25519:bvcxz"];
});
}
function bobUploadsKeys() {
bobClient.uploadKeys(5).catch(testUtils.failTest);
return expectBobKeyUpload();
}
/**
* Set an expectation that ali will query bobs keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
*/
function expectAliQueryKeys() {
// can't query keys before bob has uploaded them
expect(bobDeviceKeys).toBeDefined();
const bobKeys = {};
bobKeys[bobDeviceId] = bobDeviceKeys;
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual({});
const result = {};
result[bobUserId] = bobKeys;
return {device_keys: result};
});
return aliHttpBackend.flush("/keys/query", 1);
}
/**
* Set an expectation that bob will query alis keys; then flush the http request.
*
* @return {promise} which resolves once the http request has completed.
*/
function expectBobQueryKeys() {
// can't query keys before ali has uploaded them
expect(aliDeviceKeys).toBeDefined();
const aliKeys = {};
aliKeys[aliDeviceId] = aliDeviceKeys;
bobHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[aliUserId]).toEqual({});
const result = {};
result[aliUserId] = aliKeys;
return {device_keys: result};
});
return bobHttpBackend.flush("/keys/query", 1);
}
/**
* Set an expectation that ali will claim one of bob's keys; then flush the http request.
*
* @return {promise} resolves once the http request has completed.
*/
function expectAliClaimKeys() {
// can't query keys before bob has uploaded them
expect(bobOneTimeKeys).toBeDefined();
aliHttpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
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(claimType + ":") === 0) {
break;
}
}
}
const result = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = bobOneTimeKeys[keyId];
return {one_time_keys: result};
});
return aliHttpBackend.flush("/keys/claim", 1);
}
function aliDownloadsKeys() {
// can't query keys before bob has uploaded them
expect(bobDeviceEd25519Key).toBeDefined();
const p1 = aliClient.downloadKeys([bobUserId]).then(function() {
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([{
id: "bvcxz",
key: bobDeviceEd25519Key,
verified: false,
blocked: false,
display_name: null,
}]);
});
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() {
const devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
expect(devices[bobDeviceId].keys).toEqual(bobDeviceKeys.keys);
expect(devices[bobDeviceId].verified).
toBe(0); // DeviceVerification.UNVERIFIED
});
}
function aliEnablesEncryption() {
return aliClient.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
}).then(function() {
expect(aliClient.isRoomEncrypted(roomId)).toBeTruthy();
});
}
function bobEnablesEncryption() {
return bobClient.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
}).then(function() {
expect(bobClient.isRoomEncrypted(roomId)).toBeTruthy();
});
}
/**
* Ali sends a message, first claiming e2e keys. Set the expectations and
* check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function aliSendsFirstMessage() {
return q.all([
sendMessage(aliClient),
expectAliQueryKeys()
.then(expectAliClaimKeys)
.then(expectAliSendMessageRequest),
]).spread(function(_, ciphertext) {
return ciphertext;
});
}
/**
* Ali sends a message without first claiming e2e keys. Set the expectations
* and check the results.
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function aliSendsMessage() {
return q.all([
sendMessage(aliClient),
expectAliSendMessageRequest(),
]).spread(function(_, ciphertext) {
return ciphertext;
});
}
/**
* Bob sends a message, first querying (but not claiming) e2e keys. Set the
* expectations and check the results.
*
* @return {promise} which resolves to the ciphertext for Ali's device.
*/
function bobSendsReplyMessage() {
return q.all([
sendMessage(bobClient),
expectBobQueryKeys()
.then(expectBobSendMessageRequest),
]).spread(function(_, ciphertext) {
return ciphertext;
});
}
/**
* Set an expectation that Ali will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function expectAliSendMessageRequest() {
return expectSendMessageRequest(aliHttpBackend).then(function(content) {
aliMessages.push(content);
expect(utils.keys(content.ciphertext)).toEqual([bobDeviceCurve25519Key]);
const ciphertext = content.ciphertext[bobDeviceCurve25519Key];
expect(ciphertext).toBeDefined();
return ciphertext;
});
}
/**
* Set an expectation that Bob will send a message, and flush the request
*
* @return {promise} which resolves to the ciphertext for Bob's device.
*/
function expectBobSendMessageRequest() {
return expectSendMessageRequest(bobHttpBackend).then(function(content) {
bobMessages.push(content);
const aliKeyId = "curve25519:" + aliDeviceId;
const aliDeviceCurve25519Key = aliDeviceKeys.keys[aliKeyId];
expect(utils.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
expect(ciphertext).toBeDefined();
return ciphertext;
});
}
function sendMessage(client) {
return client.sendMessage(
roomId, {msgtype: "m.text", body: "Hello, World"},
);
}
function expectSendMessageRequest(httpBackend) {
const path = "/send/m.room.encrypted/";
let sent;
httpBackend.when("PUT", path).respond(200, function(path, content) {
sent = content;
return {
event_id: "asdfgh",
};
});
return httpBackend.flush(path, 1).then(function() {
return sent;
});
}
function aliRecvMessage() {
const message = bobMessages.shift();
return recvMessage(aliHttpBackend, aliClient, bobUserId, message);
}
function bobRecvMessage() {
const message = aliMessages.shift();
return recvMessage(bobHttpBackend, bobClient, aliUserId, message);
}
function recvMessage(httpBackend, client, sender, message) {
const syncData = {
next_batch: "x",
rooms: {
join: {
},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
testUtils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: message,
sender: sender,
}),
],
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
const deferred = q.defer();
const onEvent = function(event) {
console.log(client.credentials.userId + " received event",
event);
// ignore the m.room.member events
if (event.getType() == "m.room.member") {
return;
}
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toEqual({
msgtype: "m.text",
body: "Hello, World",
});
expect(event.isEncrypted()).toBeTruthy();
client.removeListener("event", onEvent);
deferred.resolve();
};
client.on("event", onEvent);
httpBackend.flush();
return deferred.promise;
}
function aliStartClient() {
expectAliKeyUpload().catch(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");
});
}
function bobStartClient() {
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");
});
}
/**
* Set http responses for the requests which are made when a client starts, and
* start the client.
*
* @param {object} httpBackend
* @param {MatrixClient} client
*/
function startClient(httpBackend, client) {
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
// send a sync response including our test room.
const syncData = {
next_batch: "x",
rooms: {
join: { },
},
};
syncData.rooms.join[roomId] = {
state: {
events: [
testUtils.mkMembership({
mship: "join",
user: aliUserId,
}),
testUtils.mkMembership({
mship: "join",
user: bobUserId,
}),
],
},
timeline: {
events: [],
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
}
describe("MatrixClient crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
var baseUrl = "http://localhost.or.something";
var httpBackend;
var aliClient;
var roomId = "!room:localhost";
var aliUserId = "@ali:localhost";
var aliDeviceId = "zxcvb";
var aliAccessToken = "aseukfgwef";
var bobClient;
var bobUserId = "@bob:localhost";
var bobDeviceId = "bvcxz";
var bobAccessToken = "fewgfkuesa";
var bobOneTimeKeys;
var bobDeviceKeys;
var bobDeviceCurve25519Key;
var bobDeviceEd25519Key;
var aliLocalStore;
var aliStorage;
var bobStorage;
var aliMessage;
beforeEach(function() {
aliLocalStore = new MockStorageApi();
aliStorage = new sdk.WebStorageSessionStore(aliLocalStore);
bobStorage = new sdk.WebStorageSessionStore(new MockStorageApi());
utils.beforeEach(this);
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
aliStorage = new sdk.WebStorageSessionStore(new testUtils.MockStorageApi());
aliHttpBackend = new HttpBackend();
aliClient = sdk.createClient({
baseUrl: baseUrl,
baseUrl: "http://alis.server",
userId: aliUserId,
accessToken: aliAccessToken,
deviceId: aliDeviceId,
sessionStore: aliStorage
sessionStore: aliStorage,
request: aliHttpBackend.requestFn,
});
bobStorage = new sdk.WebStorageSessionStore(new testUtils.MockStorageApi());
bobHttpBackend = new HttpBackend();
bobClient = sdk.createClient({
baseUrl: baseUrl,
baseUrl: "http://bobs.server",
userId: bobUserId,
accessToken: bobAccessToken,
deviceId: bobDeviceId,
sessionStore: bobStorage
sessionStore: bobStorage,
request: bobHttpBackend.requestFn,
});
httpBackend.when("GET", "/pushrules").respond(200, {});
bobOneTimeKeys = undefined;
aliDeviceKeys = undefined;
bobDeviceKeys = undefined;
bobDeviceCurve25519Key = undefined;
bobDeviceEd25519Key = undefined;
aliMessages = [];
bobMessages = [];
});
describe("Ali account setup", function() {
it("should have device keys", function(done) {
expect(aliClient.deviceKeys).toBeDefined();
expect(aliClient.deviceKeys.user_id).toEqual(aliUserId);
expect(aliClient.deviceKeys.device_id).toEqual(aliDeviceId);
done();
});
it("should have a curve25519 key", function(done) {
expect(aliClient.deviceCurve25519Key).toBeDefined();
done();
});
afterEach(function() {
aliClient.stopClient();
bobClient.stopClient();
});
function bobUploadsKeys(done) {
var uploadPath = "/keys/upload/bvcxz";
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.one_time_keys).toEqual({});
httpBackend.when("POST", uploadPath).respond(200, function(path, content) {
expect(content.one_time_keys).not.toEqual({});
bobDeviceKeys = content.device_keys;
bobOneTimeKeys = content.one_time_keys;
var count = 0;
for (var key in content.one_time_keys) {
if (content.one_time_keys.hasOwnProperty(key)) {
count++;
}
}
expect(count).toEqual(5);
return {one_time_key_counts: {curve25519: count}};
it('Ali knows the difference between a new user and one with no devices',
function(done) {
aliHttpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:id': {},
},
});
return {one_time_key_counts: {}};
});
bobClient.uploadKeys(5);
httpBackend.flush().done(function() {
expect(bobDeviceKeys).toBeDefined();
expect(bobOneTimeKeys).toBeDefined();
bobDeviceCurve25519Key = bobDeviceKeys.keys["curve25519:bvcxz"];
bobDeviceEd25519Key = bobDeviceKeys.keys["ed25519:bvcxz"];
done();
});
}
it("Bob uploads without one-time keys and with one-time keys", bobUploadsKeys);
const p1 = aliClient.downloadKeys(['@bob:id']);
const p2 = aliHttpBackend.flush('/keys/query', 1);
function aliDownloadsKeys(done) {
var bobKeys = {};
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(testUtils.failTest).done(done);
});
it("Ali downloads Bobs keys", function(done) {
q()
.then(bobUploadsKeys)
.then(aliDownloadsKeys)
.catch(testUtils.failTest).done(done);
});
it("Ali gets keys with an invalid signature", function(done) {
q()
.then(bobUploadsKeys)
.then(function() {
// tamper bob's keys!
expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeDefined();
bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc";
return q.all(aliClient.downloadKeys([bobUserId]),
expectAliQueryKeys());
})
.then(function() {
// should get an empty list
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
})
.catch(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;
httpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual({});
var result = {};
aliHttpBackend.when("POST", "/keys/query").respond(200, function(path, content) {
const result = {};
result[bobUserId] = bobKeys;
return {device_keys: result};
});
aliClient.downloadKeys([bobUserId]).then(function() {
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([{
id: "bvcxz",
key: bobDeviceEd25519Key
}]);
});
httpBackend.flush().done(function() {
var devices = aliStorage.getEndToEndDevicesForUser(bobUserId);
expect(devices).toEqual(bobKeys);
done();
});
}
it("Ali downloads Bobs keys", function(done) {
bobUploadsKeys(function() {aliDownloadsKeys(done);});
q.all(
aliClient.downloadKeys([bobUserId, eveUserId]),
aliHttpBackend.flush("/keys/query", 1),
).then(function() {
// should get an empty list
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
expect(aliClient.listDeviceKeys(eveUserId)).toEqual([]);
}).catch(testUtils.failTest).done(done);
});
function aliEnablesEncryption(done) {
httpBackend.when("POST", "/keys/claim").respond(200, function(path, content) {
expect(content.one_time_keys[bobUserId][bobDeviceId]).toEqual("curve25519");
for (var keyId in bobOneTimeKeys) {
if (bobOneTimeKeys.hasOwnProperty(keyId)) {
if (keyId.indexOf("curve25519:") === 0) {
break;
}
}
}
var result = {};
result[bobUserId] = {};
result[bobUserId][bobDeviceId] = {};
result[bobUserId][bobDeviceId][keyId] = bobOneTimeKeys[keyId];
return {one_time_keys: result};
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};
});
aliClient.setRoomEncryption(roomId, {
algorithm: "m.olm.v1.curve25519-aes-sha2",
members: [aliUserId, bobUserId]
}).then(function(res) {
expect(res.missingUsers).toEqual([]);
expect(res.missingDevices).toEqual({});
expect(aliClient.isRoomEncrypted(roomId)).toBeTruthy();
done();
});
httpBackend.flush();
}
q.all(
aliClient.downloadKeys([bobUserId]),
aliHttpBackend.flush("/keys/query", 1),
).then(function() {
// should get an empty list
expect(aliClient.listDeviceKeys(bobUserId)).toEqual([]);
}).catch(testUtils.failTest).done(done);
});
it("Ali enables encryption", function(done) {
bobUploadsKeys(function() {
aliDownloadsKeys(function() {
aliEnablesEncryption(done);
});
});
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.catch(testUtils.failTest).done(done);
});
function aliSendsMessage(done) {
var txnId = "a.transaction.id";
var path = "/send/m.room.encrypted/" + txnId;
httpBackend.when("PUT", path).respond(200, function(path, content) {
aliMessage = content;
expect(aliMessage.ciphertext[bobDeviceCurve25519Key]).toBeDefined();
return {};
});
aliClient.sendMessage(
roomId, {msgtype: "m.text", body: "Hello, World"}, txnId
);
httpBackend.flush().done(function() {done();});
}
it("Ali sends a message", function(done) {
bobUploadsKeys(function() {
aliDownloadsKeys(function() {
aliEnablesEncryption(function() {
aliSendsMessage(done);
});
});
});
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.catch(testUtils.failTest).nodeify(done);
});
function bobRecvMessage(done) {
var initialSync = {
end: "alpha",
presence: [],
rooms: []
};
var events = {
start: "alpha",
end: "beta",
chunk: [utils.mkEvent({
type: "m.room.encrypted",
room: roomId,
content: aliMessage
})]
};
httpBackend.when("GET", "initialSync").respond(200, initialSync);
httpBackend.when("GET", "events").respond(200, events);
bobClient.on("event", function(event) {
expect(event.getType()).toEqual("m.room.message");
expect(event.getContent()).toEqual({
msgtype: "m.text",
body: "Hello, World"
});
expect(event.isEncrypted()).toBeTruthy();
done();
});
bobClient.startClient();
httpBackend.flush();
}
it("Bob receives a message", function(done) {
bobUploadsKeys(function() {
aliDownloadsKeys(function() {
aliEnablesEncryption(function() {
aliSendsMessage(function() {
bobRecvMessage(done);
});
});
});
});
}, 30000); //timeout after 30s
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobStartClient)
.then(bobRecvMessage)
.catch(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) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliDownloadsKeys)
.then(function() {
aliClient.setDeviceBlocked(bobUserId, bobDeviceId, true);
const p1 = sendMessage(aliClient);
const p2 = expectAliQueryKeys()
.then(expectAliClaimKeys)
.then(function() {
return expectSendMessageRequest(aliHttpBackend);
}).then(function(sentContent) {
// no unblocked devices, so the ciphertext should be empty
expect(sentContent.ciphertext).toEqual({});
});
return q.all([p1, p2]);
}).catch(testUtils.failTest).nodeify(done);
});
it("Bob receives two pre-key messages", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobStartClient)
.then(bobRecvMessage)
.then(aliSendsMessage)
.then(bobRecvMessage)
.catch(testUtils.failTest).done(done);
});
it("Bob replies to the message", function(done) {
q()
.then(bobUploadsKeys)
.then(aliStartClient)
.then(aliEnablesEncryption)
.then(aliSendsFirstMessage)
.then(bobStartClient)
.then(bobRecvMessage)
.then(bobEnablesEncryption)
.then(bobSendsReplyMessage).then(function(ciphertext) {
expect(ciphertext.type).toEqual(1);
}).then(aliRecvMessage)
.catch(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);
});
});
+155 -126
View File
@@ -1,143 +1,155 @@
"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" });
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("emissions", function() {
var initialSync = {
end: "s_5_3",
presence: [{
event_id: "$wefiuewh:bar",
type: "m.presence",
content: {
user_id: "@foo:bar",
displayname: "Foo Bar",
presence: "online"
}
}],
rooms: [{
room_id: "!erufh:bar",
membership: "join",
messages: {
start: "s",
end: "t",
chunk: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm"
})
]
},
state: [
utils.mkMembership({
room: "!erufh:bar", mship: "join", user: "@foo:bar"
const SYNC_DATA = {
next_batch: "s_5_3",
presence: {
events: [
utils.mkPresence({
user: "@foo:bar", name: "Foo Bar", presence: "online",
}),
utils.mkEvent({
type: "m.room.create", room: "!erufh:bar", user: "@foo:bar",
content: {
creator: "@foo:bar"
}
})
]
}]
],
},
rooms: {
join: {
"!erufh:bar": {
timeline: {
events: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "hmmm",
}),
],
prev_batch: "s",
},
state: {
events: [
utils.mkMembership({
room: "!erufh:bar", mship: "join", user: "@foo:bar",
}),
utils.mkEvent({
type: "m.room.create", room: "!erufh:bar",
user: "@foo:bar",
content: {
creator: "@foo:bar",
},
}),
],
},
},
},
},
};
var eventData = {
start: "s_5_3",
end: "e_6_7",
chunk: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: "ello ello"
}),
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: ":D"
}),
utils.mkEvent({
type: "m.typing", room: "!erufh:bar", content: {
user_ids: ["@foo:bar"]
}
})
]
const NEXT_SYNC_DATA = {
next_batch: "e_6_7",
rooms: {
join: {
"!erufh:bar": {
timeline: {
events: [
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar",
msg: "ello ello",
}),
utils.mkMessage({
room: "!erufh:bar", user: "@foo:bar", msg: ":D",
}),
],
},
ephemeral: {
events: [
utils.mkEvent({
type: "m.typing", room: "!erufh:bar", content: {
user_ids: ["@foo:bar"],
},
}),
],
},
},
},
},
};
it("should emit events from both /initialSync and /events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
it("should emit events from both the first and subsequent /sync calls",
function(done) {
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
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,
);
// initial sync events are unordered, so make an array of the types
// that should be emitted and we'll just pick them off one by one,
// so long as this is emptied we're good.
var initialSyncEventTypes = [
"m.presence", "m.room.member", "m.room.message", "m.room.create"
];
var chunkIndex = 0;
client.on("event", function(event) {
if (initialSyncEventTypes.length === 0) {
if (chunkIndex + 1 >= eventData.chunk.length) {
return;
let found = false;
for (let i = 0; i < expectedEvents.length; i++) {
if (expectedEvents[i].event_id === event.getId()) {
expectedEvents.splice(i, 1);
found = true;
break;
}
// this should be /events now
expect(eventData.chunk[chunkIndex].event_id).toEqual(
event.getId()
);
chunkIndex++;
return;
}
var index = initialSyncEventTypes.indexOf(event.getType());
expect(index).not.toEqual(
-1, "Unexpected event type: " + event.getType()
expect(found).toBe(
true, "Unexpected 'event' emitted: " + event.getType(),
);
if (index >= 0) {
initialSyncEventTypes.splice(index, 1);
}
});
client.startClient();
httpBackend.flush().done(function() {
expect(initialSyncEventTypes.length).toEqual(
0, "Failed to see all events from /initialSync"
);
expect(chunkIndex + 1).toEqual(
eventData.chunk.length, "Failed to see all events from /events"
expect(expectedEvents.length).toEqual(
0, "Failed to see all events from /sync calls",
);
done();
});
});
it("should emit User events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
var fired = false;
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
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(initialSync.presence[0]);
expect(event.event).toEqual(SYNC_DATA.presence.events[0]);
expect(user.presence).toEqual(
initialSync.presence[0].content.presence
SYNC_DATA.presence.events[0].content.presence,
);
});
client.startClient();
@@ -149,11 +161,11 @@ describe("MatrixClient events", function() {
});
it("should emit Room events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
var roomInvokeCount = 0;
var roomNameInvokeCount = 0;
var timelineFireCount = 0;
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
let roomInvokeCount = 0;
let roomNameInvokeCount = 0;
let timelineFireCount = 0;
client.on("Room", function(room) {
roomInvokeCount++;
expect(room.roomId).toEqual("!erufh:bar");
@@ -170,33 +182,33 @@ 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();
});
});
it("should emit RoomState events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var roomStateEventTypes = [
"m.room.member", "m.room.create"
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);
@@ -219,26 +231,26 @@ 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();
});
});
it("should emit RoomMember events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
var typingInvokeCount = 0;
var powerLevelInvokeCount = 0;
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++;
});
@@ -258,20 +270,37 @@ 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();
});
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function(done) {
httpBackend.when("GET", "/sync").respond(401, { errcode: 'M_UNKNOWN_TOKEN' });
let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(event, member) {
sessionLoggedOutCount++;
});
client.startClient();
httpBackend.flush().done(function() {
expect(sessionLoggedOutCount).toEqual(
1, "Session.logged_out fired wrong number of times",
);
done();
});
});
});
});
@@ -0,0 +1,738 @@
"use strict";
const q = require("q");
const sdk = require("../..");
const HttpBackend = require("../mock-request");
const utils = require("../test-utils");
const EventTimeline = sdk.EventTimeline;
const baseUrl = "http://localhost.or.something";
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,
});
const ROOM_NAME_EVENT = utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId,
content: {
name: "Old room name",
},
});
const INITIAL_SYNC_DATA = {
next_batch: "s_5_3",
rooms: {
join: {
"!foo:bar": { // roomId
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello",
}),
],
prev_batch: "f_1_1",
},
state: {
events: [
ROOM_NAME_EVENT,
utils.mkMembership({
room: roomId, mship: "join",
user: otherUserId, name: "Bob",
}),
USER_MEMBERSHIP_EVENT,
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId,
},
}),
],
},
},
},
},
};
const EVENTS = [
utils.mkMessage({
room: roomId, user: userId, msg: "we",
}),
utils.mkMessage({
room: roomId, user: userId, msg: "could",
}),
utils.mkMessage({
room: roomId, user: userId, msg: "be",
}),
utils.mkMessage({
room: roomId, user: userId, msg: "heroes",
}),
];
// start the client, and wait for it to initialise
function startClient(httpBackend, client) {
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, INITIAL_SYNC_DATA);
client.startClient();
// set up a promise which will resolve once the client is initialised
const deferred = q.defer();
client.on("sync", function(state) {
console.log("sync", state);
if (state != "SYNCING") {
return;
}
deferred.resolve();
});
httpBackend.flush();
return deferred.promise;
}
describe("getEventTimeline support", function() {
let httpBackend;
let client;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
});
afterEach(function() {
if (client) {
client.stopClient();
}
});
it("timeline support must be enabled to work", function(done) {
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
});
startClient(httpBackend, client,
).then(function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
expect(function() {
client.getEventTimeline(timelineSet, "event");
}).toThrow();
}).catch(utils.failTest).done(done);
});
it("timeline support works when enabled", function(done) {
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
timelineSupport: true,
});
startClient(httpBackend, client,
).then(function() {
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);
});
it("scrollback should be able to scroll back to before a gappy /sync",
function(done) {
// need a client with timelineSupport disabled to make this work
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
});
let room;
startClient(httpBackend, client,
).then(function() {
room = client.getRoom(roomId);
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
EVENTS[0],
],
prev_batch: "f_1_1",
},
},
},
},
});
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_5",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
EVENTS[1],
],
limited: true,
prev_batch: "f_1_2",
},
},
},
},
});
httpBackend.when("GET", "/messages").respond(200, {
chunk: [EVENTS[0]],
start: "pagin_start",
end: "pagin_end",
});
return httpBackend.flush("/sync", 2);
}).then(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(EVENTS[1]);
httpBackend.flush("/messages", 1);
return client.scrollback(room);
}).then(function() {
expect(room.timeline.length).toEqual(2);
expect(room.timeline[0].event).toEqual(EVENTS[0]);
expect(room.timeline[1].event).toEqual(EVENTS[1]);
expect(room.oldState.paginationToken).toEqual("pagin_end");
}).catch(utils.failTest).done(done);
});
});
describe("MatrixClient event timelines", function() {
let client = null;
let httpBackend = null;
beforeEach(function(done) {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
timelineSupport: true,
});
startClient(httpBackend, client)
.catch(utils.failTest).done(done);
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("getEventTimeline", function() {
it("should create a new timeline for new events", function(done) {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/event1%3Abar")
.respond(200, function() {
return {
start: "start_token",
events_before: [EVENTS[1], EVENTS[0]],
event: EVENTS[2],
events_after: [EVENTS[3]],
state: [
ROOM_NAME_EVENT,
USER_MEMBERSHIP_EVENT,
],
end: "end_token",
};
});
client.getEventTimeline(timelineSet, "event1:bar").then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
for (let i = 0; i < 4; i++) {
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
expect(tl.getEvents()[i].sender.name).toEqual(userName);
}
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
it("should return existing timeline for known events", function(done) {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
EVENTS[0],
],
prev_batch: "f_1_2",
},
},
},
},
});
httpBackend.flush("/sync").then(function() {
return client.getEventTimeline(timelineSet, EVENTS[0].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(EVENTS[0]);
expect(tl.getEvents()[1].sender.name).toEqual(userName);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("f_1_1");
// expect(tl.getPaginationToken(EventTimeline.FORWARDS)).toEqual("s_5_4");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
it("should update timelines where they overlap a previous /sync", function(done) {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
EVENTS[3],
],
prev_batch: "f_1_2",
},
},
},
},
});
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[2].event_id))
.respond(200, function() {
return {
start: "start_token",
events_before: [EVENTS[1]],
event: EVENTS[2],
events_after: [EVENTS[3]],
end: "end_token",
state: [],
};
});
client.on("sync", function() {
client.getEventTimeline(timelineSet, EVENTS[2].event_id,
).then(function(tl) {
expect(tl.getEvents().length).toEqual(4);
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
expect(tl.getEvents()[3].event).toEqual(EVENTS[3]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token");
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
// .toEqual("s_5_4");
}).catch(utils.failTest).done(done);
});
httpBackend.flush().catch(utils.failTest);
});
it("should join timelines where they overlap a previous /context",
function(done) {
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/" +
encodeURIComponent(EVENTS[0].event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: EVENTS[0],
events_after: [],
end: "end_token0",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[2].event_id))
.respond(200, function() {
return {
start: "start_token2",
events_before: [],
event: EVENTS[2],
events_after: [],
end: "end_token2",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[3].event_id))
.respond(200, function() {
return {
start: "start_token3",
events_before: [],
event: EVENTS[3],
events_after: [],
end: "end_token3",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[1].event_id))
.respond(200, function() {
return {
start: "start_token4",
events_before: [EVENTS[0]],
event: EVENTS[1],
events_after: [EVENTS[2], EVENTS[3]],
end: "end_token4",
state: [],
};
});
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);
return client.getEventTimeline(timelineSet, EVENTS[3].event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(1);
tl3 = tl;
return client.getEventTimeline(timelineSet, EVENTS[1].event_id);
}).then(function(tl) {
// we expect it to get merged in with event 2
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
expect(tl.getNeighbouringTimeline(EventTimeline.BACKWARDS))
.toBe(tl0);
expect(tl.getNeighbouringTimeline(EventTimeline.FORWARDS))
.toBe(tl3);
expect(tl0.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl0.getPaginationToken(EventTimeline.FORWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.BACKWARDS))
.toBe(null);
expect(tl3.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token3");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
it("should fail gracefully if there is no event field", function(done) {
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")
.respond(200, function() {
return {
start: "start_token",
events_before: [],
events_after: [],
end: "end_token",
state: [],
};
});
client.getEventTimeline(timelineSet, "event1",
).then(function(tl) {
// could do with a fail()
expect(true).toBeFalsy();
}).catch(function(e) {
expect(String(e)).toMatch(/'event'/);
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
});
describe("paginateEventTimeline", function() {
it("should allow you to paginate backwards", function(done) {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: EVENTS[0],
events_after: [],
end: "end_token0",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
.check(function(req) {
const params = req.queryParams;
expect(params.dir).toEqual("b");
expect(params.from).toEqual("start_token0");
expect(params.limit).toEqual(30);
}).respond(200, function() {
return {
chunk: [EVENTS[1], EVENTS[2]],
end: "start_token1",
};
});
let tl;
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(tl, {backwards: true});
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[2]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[0]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token1");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token0");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
it("should allow you to paginate forwards", function(done) {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/context/" +
encodeURIComponent(EVENTS[0].event_id))
.respond(200, function() {
return {
start: "start_token0",
events_before: [],
event: EVENTS[0],
events_after: [],
end: "end_token0",
state: [],
};
});
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
.check(function(req) {
const params = req.queryParams;
expect(params.dir).toEqual("f");
expect(params.from).toEqual("end_token0");
expect(params.limit).toEqual(20);
}).respond(200, function() {
return {
chunk: [EVENTS[1], EVENTS[2]],
end: "end_token1",
};
});
let tl;
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
).then(function(tl0) {
tl = tl0;
return client.paginateEventTimeline(
tl, {backwards: false, limit: 20});
}).then(function(success) {
expect(success).toBeTruthy();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[0].event).toEqual(EVENTS[0]);
expect(tl.getEvents()[1].event).toEqual(EVENTS[1]);
expect(tl.getEvents()[2].event).toEqual(EVENTS[2]);
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token0");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token1");
}).catch(utils.failTest).done(done);
httpBackend.flush().catch(utils.failTest);
});
});
describe("event timeline for sent events", function() {
const TXN_ID = "txn1";
const event = utils.mkMessage({
room: roomId, user: userId, msg: "a body",
});
event.unsigned = {transaction_id: TXN_ID};
beforeEach(function() {
// set up handlers for both the message send, and the
// /sync
httpBackend.when("PUT", "/send/m.room.message/" + TXN_ID)
.respond(200, {
event_id: event.event_id,
});
httpBackend.when("GET", "/sync").respond(200, {
next_batch: "s_5_4",
rooms: {
join: {
"!foo:bar": {
timeline: {
events: [
event,
],
prev_batch: "f_1_1",
},
},
},
},
});
});
it("should work when /send returns before /sync", function(done) {
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);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
// 2 because the initial sync contained an event
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
// now let the sync complete, and check it again
return httpBackend.flush("/sync", 1);
}).then(function() {
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
}).catch(utils.failTest).done(done);
httpBackend.flush("/send/m.room.message/" + TXN_ID, 1).catch(utils.failTest);
});
it("should work when /send returns after /sync", function(done) {
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.
client.sendTextMessage(roomId, "a body", TXN_ID).then(function(res) {
console.log("sendTextMessage completed");
expect(res.event_id).toEqual(event.event_id);
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (2)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].getContent().body).toEqual("a body");
}).catch(utils.failTest).done(done);
httpBackend.flush("/sync", 1).then(function() {
return client.getEventTimeline(timelineSet, event.event_id);
}).then(function(tl) {
console.log("getEventTimeline completed (1)");
expect(tl.getEvents().length).toEqual(2);
expect(tl.getEvents()[1].event).toEqual(event);
// now let the send complete.
return httpBackend.flush("/send/m.room.message/" + TXN_ID, 1);
}).catch(utils.failTest);
});
});
it("should handle gappy syncs after redactions", function(done) {
// https://github.com/vector-im/vector-web/issues/1389
// a state event, followed by a redaction thereof
const event = utils.mkMembership({
room: roomId, mship: "join", user: otherUserId,
});
const redaction = utils.mkEvent({
type: "m.room.redaction",
room_id: roomId,
sender: otherUserId,
content: {},
});
redaction.redacts = event.event_id;
const syncData = {
next_batch: "batch1",
rooms: {
join: {},
},
};
syncData.rooms.join[roomId] = {
timeline: {
events: [
event,
redaction,
],
limited: false,
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.flush().then(function() {
const room = client.getRoom(roomId);
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(3);
expect(tl.getEvents()[1].isRedacted()).toBe(true);
const sync2 = {
next_batch: "batch2",
rooms: {
join: {},
},
};
sync2.rooms.join[roomId] = {
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "world",
}),
],
limited: true,
prev_batch: "newerTok",
},
};
httpBackend.when("GET", "/sync").respond(200, sync2);
return httpBackend.flush();
}).then(function() {
const room = client.getRoom(roomId);
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(1);
}).catch(utils.failTest).done(done);
});
});
+377 -17
View File
@@ -1,27 +1,38 @@
"use strict";
var sdk = require("../..");
var HttpBackend = require("../mock-request");
var publicGlobals = require("../../lib/matrix");
var Room = publicGlobals.Room;
var MatrixInMemoryStore = publicGlobals.MatrixInMemoryStore;
var utils = require("../test-utils");
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;
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();
const mockStorage = new MockStorageApi();
sessionStore = new sdk.WebStorageSessionStore(mockStorage);
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
deviceId: "aliceDevice",
accessToken: accessToken,
store: store
store: store,
sessionStore: sessionStore,
});
});
@@ -29,18 +40,367 @@ 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);
room.addEvents([
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);
httpBackend.verifyNoOutstandingRequests();
});
});
describe("getFilter", function() {
const filterId = "f1lt3r1d";
it("should return a filter from the store if allowCached", function(done) {
const filter = Filter.fromJson(userId, filterId, {
event_format: "client",
});
store.storeFilter(filter);
client.getFilter(userId, filterId, true).done(function(gotFilter) {
expect(gotFilter).toEqual(filter);
done();
});
httpBackend.verifyNoOutstandingRequests();
});
it("should do an HTTP request if !allowCached even if one exists",
function(done) {
const httpFilterDefinition = {
event_format: "federation",
};
httpBackend.when(
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
).respond(200, httpFilterDefinition);
const storeFilter = Filter.fromJson(userId, filterId, {
event_format: "client",
});
store.storeFilter(storeFilter);
client.getFilter(userId, filterId, false).done(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
done();
});
httpBackend.flush();
});
it("should do an HTTP request if nothing is in the cache and then store it",
function(done) {
const httpFilterDefinition = {
event_format: "federation",
};
expect(store.getFilter(userId, filterId)).toBeNull();
httpBackend.when(
"GET", "/user/" + encodeURIComponent(userId) + "/filter/" + filterId,
).respond(200, httpFilterDefinition);
client.getFilter(userId, filterId, true).done(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(httpFilterDefinition);
expect(store.getFilter(userId, filterId)).toBeDefined();
done();
});
httpBackend.flush();
});
});
describe("createFilter", function() {
const filterId = "f1llllllerid";
it("should do an HTTP request and then store the filter", function(done) {
expect(store.getFilter(userId, filterId)).toBeNull();
const filterDefinition = {
event_format: "client",
};
httpBackend.when(
"POST", "/user/" + encodeURIComponent(userId) + "/filter",
).check(function(req) {
expect(req.data).toEqual(filterDefinition);
}).respond(200, {
filter_id: filterId,
});
client.createFilter(filterDefinition).done(function(gotFilter) {
expect(gotFilter.getDefinition()).toEqual(filterDefinition);
expect(store.getFilter(userId, filterId)).toEqual(gotFilter);
done();
});
httpBackend.flush();
});
});
describe("searching", function() {
const response = {
search_categories: {
room_events: {
count: 24,
results: {
"$flibble:localhost": {
rank: 0.1,
result: {
type: "m.room.message",
user_id: "@alice:localhost",
room_id: "!feuiwhf:localhost",
content: {
body: "a result",
msgtype: "m.text",
},
},
},
},
},
},
};
it("searchMessageText should perform a /search for room_events", function(done) {
client.searchMessageText({
query: "monkeys",
});
httpBackend.when("POST", "/search").check(function(req) {
expect(req.data).toEqual({
search_categories: {
room_events: {
search_term: "monkeys",
},
},
});
}).respond(200, response);
httpBackend.flush().done(function() {
done();
});
});
});
describe("downloadKeys", function() {
it("should do an HTTP request and then store the keys", function(done) {
const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
// ed25519key = client.getDeviceEd25519Key();
const borisKeys = {
dev1: {
algorithms: ["1"],
device_id: "dev1",
keys: { "ed25519:dev1": ed25519key },
signatures: {
boris: {
"ed25519:dev1":
"RAhmbNDq1efK3hCpBzZDsKoGSsrHUxb25NW5/WbEV9R" +
"JVwLdP032mg5QsKt/pBDUGtggBcnk43n3nBWlA88WAw",
},
},
unsigned: { "abc": "def" },
user_id: "boris",
},
};
const chazKeys = {
dev2: {
algorithms: ["2"],
device_id: "dev2",
keys: { "ed25519:dev2": ed25519key },
signatures: {
chaz: {
"ed25519:dev2":
"FwslH/Q7EYSb7swDJbNB5PSzcbEO1xRRBF1riuijqvL" +
"EkrK9/XVN8jl4h7thGuRITQ01siBQnNmMK9t45QfcCQ",
},
},
unsigned: { "ghi": "def" },
user_id: "chaz",
},
};
/*
function sign(o) {
var anotherjson = require('another-json');
var b = JSON.parse(JSON.stringify(o));
delete(b.signatures);
delete(b.unsigned);
return client._crypto._olmDevice.sign(anotherjson.stringify(b));
};
console.log("Ed25519: " + ed25519key);
console.log("boris:", sign(borisKeys.dev1));
console.log("chaz:", sign(chazKeys.dev2));
*/
httpBackend.when("POST", "/keys/query").check(function(req) {
expect(req.data).toEqual({device_keys: {
'@alice:localhost': {},
'boris': {},
'chaz': {},
}});
}).respond(200, {
device_keys: {
boris: borisKeys,
chaz: chazKeys,
},
});
client.downloadKeys(["boris", "chaz"]).then(function(res) {
assertObjectContains(res.boris.dev1, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev1": ed25519key },
algorithms: ["1"],
unsigned: { "abc": "def" },
});
assertObjectContains(res.chaz.dev2, {
verified: 0, // DeviceVerification.UNVERIFIED
keys: { "ed25519:dev2": ed25519key },
algorithms: ["2"],
unsigned: { "ghi": "def" },
});
}).catch(utils.failTest).done(done);
httpBackend.flush();
});
});
describe("deleteDevice", function() {
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);
client.deleteDevice(
"my_device", auth,
).catch(utils.failTest).done(done);
httpBackend.flush();
});
});
});
function assertObjectContains(obj, expected) {
for (const k in expected) {
if (expected.hasOwnProperty(k)) {
expect(obj[k]).toEqual(expected[k]);
}
}
}
+73 -70
View File
@@ -1,61 +1,60 @@
"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 eventData = {
chunk: [],
start: "s",
end: "e"
};
var initialSync = {
end: "s_5_3",
presence: [],
rooms: [{
membership: "join",
room_id: roomId,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage({
room: roomId, user: userB, msg: "hello"
})
]
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: {
join: {
"!foo:bar": { // roomId
timeline: {
events: [
utils.mkMessage({
room: roomId, user: userB, msg: "hello",
}),
],
prev_batch: "f_1_1",
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB,
content: {
name: "Old room name",
},
}),
utils.mkMembership({
room: roomId, mship: "join", user: userB, name: "Bob",
}),
utils.mkMembership({
room: roomId, mship: "join", user: userId, name: "Alice",
}),
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId,
},
}),
],
},
},
},
state: [
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB,
content: {
name: "Old room name"
}
}),
utils.mkMembership({
room: roomId, mship: "join", user: userB, name: "Bob"
}),
utils.mkMembership({
room: roomId, mship: "join", user: userId, name: "Alice"
}),
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}]
},
};
beforeEach(function() {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
});
@@ -71,14 +70,18 @@ describe("MatrixClient opts", function() {
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
scheduler: new sdk.MatrixScheduler()
scheduler: new sdk.MatrixScheduler(),
});
});
afterEach(function() {
client.stopClient();
});
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);
@@ -88,29 +91,29 @@ 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, {});
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" });
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush("/pushrules", 1).then(function() {
return httpBackend.flush("/initialSync", 1);
return httpBackend.flush("/filter", 1);
}).then(function() {
return httpBackend.flush("/events", 1);
return httpBackend.flush("/sync", 1);
}).done(function() {
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes
0, "Expected to see event types: " + expectedEventTypes,
);
done();
});
@@ -125,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");
@@ -145,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);
@@ -169,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");
+63 -9
View File
@@ -1,23 +1,32 @@
"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");
const EventStatus = sdk.EventStatus;
describe("MatrixClient retrying", function() {
var baseUrl = "http://localhost.or.something";
var client, httpBackend;
var userId = "@alice:localhost";
var accessToken = "aseukfgwef";
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();
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken
accessToken: accessToken,
scheduler: scheduler,
});
room = new sdk.Room(roomId);
client.store.storeRoom(room);
});
afterEach(function() {
@@ -40,6 +49,51 @@ 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
client.sendMessage(roomId, "m1").then(function(ev) {
expect(ev).toEqual(ev1);
});
client.sendMessage(roomId, "m2").then(function(ev) {
expect(ev).toEqual(ev2);
});
// both events should be in the timeline at this point
const tl = room.getLiveTimeline().getEvents();
expect(tl.length).toEqual(2);
const ev1 = tl[0];
const ev2 = tl[1];
expect(ev1.status).toEqual(EventStatus.SENDING);
expect(ev2.status).toEqual(EventStatus.SENDING);
// the first message should get sent, and the second should get queued
httpBackend.when("PUT", "/send/m.room.message/").check(function(rq) {
// ev2 should now have been queued
expect(ev2.status).toEqual(EventStatus.QUEUED);
// now we can cancel the second and check everything looks sane
client.cancelPendingEvent(ev2);
expect(ev2.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(1);
// shouldn't be able to cancel the first message yet
expect(function() {
client.cancelPendingEvent(ev1);
}).toThrow();
}).respond(400); // fail the first message
httpBackend.flush().then(function() {
expect(ev1.status).toEqual(EventStatus.NOT_SENT);
expect(tl.length).toEqual(1);
// cancel the first message
client.cancelPendingEvent(ev1);
expect(ev1.status).toEqual(EventStatus.CANCELLED);
expect(tl.length).toEqual(0);
}).catch(utils.failTest).done(done);
});
describe("resending", function() {
xit("should be able to resend a NOT_SENT event", function() {
+361 -188
View File
@@ -1,89 +1,141 @@
"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 eventData;
var initialSync = {
end: "s_5_3",
presence: [],
rooms: [{
membership: "join",
room_id: roomId,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello"
})
]
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,
});
const ROOM_NAME_EVENT = utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId,
content: {
name: "Old room name",
},
});
let NEXT_SYNC_DATA;
const SYNC_DATA = {
next_batch: "s_5_3",
rooms: {
join: {
"!foo:bar": { // roomId
timeline: {
events: [
utils.mkMessage({
room: roomId, user: otherUserId, msg: "hello",
}),
],
prev_batch: "f_1_1",
},
state: {
events: [
ROOM_NAME_EVENT,
utils.mkMembership({
room: roomId, mship: "join",
user: otherUserId, name: "Bob",
}),
USER_MEMBERSHIP_EVENT,
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId,
},
}),
],
},
},
},
state: [
utils.mkEvent({
type: "m.room.name", room: roomId, user: otherUserId,
content: {
name: "Old room name"
}
}),
utils.mkMembership({
room: roomId, mship: "join", user: otherUserId, name: "Bob"
}),
utils.mkMembership({
room: roomId, mship: "join", user: userId, name: userName
}),
utils.mkEvent({
type: "m.room.create", room: roomId, user: userId,
content: {
creator: userId
}
})
]
}]
},
};
function setNextSyncData(events) {
events = events || [];
NEXT_SYNC_DATA = {
next_batch: "n",
presence: { events: [] },
rooms: {
invite: {},
join: {
"!foo:bar": {
timeline: { events: [] },
state: { events: [] },
ephemeral: { events: [] },
},
},
leave: {},
},
};
events.forEach(function(e) {
if (e.room_id !== roomId) {
throw new Error("setNextSyncData only works with one room id");
}
if (e.state_key) {
if (e.__prev_event === undefined) {
throw new Error(
"setNextSyncData needs the prev state set to '__prev_event' " +
"for " + e.type,
);
}
if (e.__prev_event !== null) {
// push the previous state for this event type
NEXT_SYNC_DATA.rooms.join[roomId].state.events.push(e.__prev_event);
}
// push the current
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
} else if (["m.typing", "m.receipt"].indexOf(e.type) !== -1) {
NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e);
} else {
NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e);
}
});
}
beforeEach(function(done) {
utils.beforeEach(this);
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken
accessToken: accessToken,
// these tests should work with or without timelineSupport
timelineSupport: true,
});
eventData = {
chunk: [],
end: "end_",
start: "start_"
};
setNextSyncData();
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, function() {
return eventData;
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
httpBackend.when("GET", "/sync").respond(200, SYNC_DATA);
httpBackend.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
client.startClient();
httpBackend.flush("/pushrules").done(done);
httpBackend.flush("/pushrules").then(function() {
return httpBackend.flush("/filter");
}).done(done);
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("local echo events", function() {
it("should be added immediately after calling MatrixClient.sendEvent " +
"with EventStatus.SENDING and the right event.sender", function(done) {
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.sendTextMessage(roomId, "I am a fish", "txn1");
@@ -92,64 +144,71 @@ 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);
httpBackend.flush("/events", 1).done(function() {
httpBackend.flush("/sync", 1).done(function() {
done();
});
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should be updated correctly when the send request finishes " +
"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,
});
eventData.chunk = [
utils.mkMessage({
body: "I am a fish", user: userId, room: roomId
})
];
eventData.chunk[0].event_id = eventId;
client.on("syncComplete", function() {
var room = client.getRoom(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;
}
const room = client.getRoom(roomId);
client.sendTextMessage(roomId, "I am a fish", "txn1").done(
function() {
expect(room.timeline[1].getId()).toEqual(eventId);
httpBackend.flush("/events", 1).done(function() {
httpBackend.flush("/sync", 1).done(function() {
expect(room.timeline[1].getId()).toEqual(eventId);
done();
});
});
httpBackend.flush("/txn1", 1);
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should be updated correctly when the send request finishes " +
"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,
});
eventData.chunk = [
utils.mkMessage({
body: "I am a fish", user: userId, room: roomId
})
];
eventData.chunk[0].event_id = eventId;
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
var promise = client.sendTextMessage(roomId, "I am a fish", "txn1");
httpBackend.flush("/events", 1).done(function() {
// expect 3rd msg, it doesn't know this is the request is just did
expect(room.timeline.length).toEqual(3);
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;
}
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);
promise.done(function() {
expect(room.timeline.length).toEqual(2);
@@ -157,15 +216,14 @@ describe("MatrixClient room timelines", function() {
done();
});
});
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
});
describe("paginated events", function() {
var sbEvents;
var sbEndTok = "pagin_end";
let sbEvents;
const sbEndTok = "pagin_end";
beforeEach(function() {
sbEvents = [];
@@ -173,15 +231,18 @@ describe("MatrixClient room timelines", function() {
return {
chunk: sbEvents,
start: "pagin_start",
end: sbEndTok
end: sbEndTok,
};
});
});
it("should set Room.oldState.paginationToken to null at the start" +
" of the timeline.", function(done) {
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.scrollback(room).done(function() {
@@ -191,66 +252,93 @@ describe("MatrixClient room timelines", function() {
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1);
httpBackend.flush("/sync", 1);
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should set the right event.sender values", function(done) {
// make an m.room.member event with prev_content
var oldMshipEvent = utils.mkMembership({
// We're aiming for an eventual timeline of:
//
// 'Old Alice' joined the room
// <Old Alice> I'm old alice
// @alice:localhost changed their name from 'Old Alice' to 'Alice'
// <Alice> I'm alice
// ------^ /messages results above this point, /sync result below
// <Bob> hello
// make an m.room.member event for alice's join
const joinMshipEvent = utils.mkMembership({
mship: "join", user: userId, room: roomId, name: "Old Alice",
url: null,
});
// make an m.room.member event with prev_content for alice's nick
// change
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
// 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("syncComplete", function() {
var room = client.getRoom(roomId);
client.on("sync", function(state) {
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(4);
var oldMsg = room.timeline[0];
expect(room.timeline.length).toEqual(5);
const joinMsg = room.timeline[0];
expect(joinMsg.sender.name).toEqual("Old Alice");
const oldMsg = room.timeline[1];
expect(oldMsg.sender.name).toEqual("Old Alice");
var newMsg = room.timeline[2];
const newMsg = room.timeline[3];
expect(newMsg.sender.name).toEqual(userName);
done();
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1);
httpBackend.flush("/sync", 1);
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should add it them to the right place in the timeline", function(done) {
// 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("syncComplete", function() {
var room = client.getRoom(roomId);
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
expect(room.timeline.length).toEqual(1);
client.scrollback(room).done(function() {
@@ -261,153 +349,238 @@ describe("MatrixClient room timelines", function() {
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1);
httpBackend.flush("/sync", 1);
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should use 'end' as the next pagination token", function(done) {
// 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("syncComplete", function() {
var room = client.getRoom(roomId);
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
expect(room.oldState.paginationToken).toBeDefined();
client.scrollback(room, 1).done(function() {
expect(room.oldState.paginationToken).toEqual(sbEndTok);
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1).done(function() {
done();
});
httpBackend.flush("/sync", 1);
httpBackend.flush("/messages", 1).done(function() {
done();
});
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
});
describe("new events", function() {
it("should be added to the right place in the timeline", function(done) {
eventData.chunk = [
const eventData = [
utils.mkMessage({user: userId, room: roomId}),
utils.mkMessage({user: userId, room: roomId}),
utils.mkMessage({user: userId, room: roomId})
];
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
setNextSyncData(eventData);
var index = 0;
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
let index = 0;
client.on("Room.timeline", function(event, rm, toStart) {
expect(toStart).toBe(false);
expect(rm).toEqual(room);
expect(event.event).toEqual(eventData.chunk[index]);
expect(event.event).toEqual(eventData[index]);
index += 1;
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/events", 1).done(function() {
httpBackend.flush("/sync", 1).then(function() {
expect(index).toEqual(2);
expect(room.timeline[room.timeline.length - 1].event).toEqual(
eventData.chunk[1]
expect(room.timeline.length).toEqual(3);
expect(room.timeline[2].event).toEqual(
eventData[1],
);
expect(room.timeline[room.timeline.length - 2].event).toEqual(
eventData.chunk[0]
expect(room.timeline[1].event).toEqual(
eventData[0],
);
done();
});
}).catch(utils.failTest).done(done);
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should set the right event.sender values", function(done) {
eventData.chunk = [
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}),
];
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
httpBackend.flush("/events", 1).done(function() {
var preNameEvent = room.timeline[room.timeline.length - 3];
var postNameEvent = room.timeline[room.timeline.length - 1];
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
setNextSyncData(eventData);
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
httpBackend.flush("/sync", 1).then(function() {
const preNameEvent = room.timeline[room.timeline.length - 3];
const postNameEvent = room.timeline[room.timeline.length - 1];
expect(preNameEvent.sender.name).toEqual(userName);
expect(postNameEvent.sender.name).toEqual("New Name");
done();
});
}).catch(utils.failTest).done(done);
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should set the right room.name", function(done) {
eventData.chunk = [
utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 2"
}
})
];
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
var nameEmitCount = 0;
const secondRoomNameEvent = utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 2",
},
});
secondRoomNameEvent.__prev_event = ROOM_NAME_EVENT;
setNextSyncData([secondRoomNameEvent]);
client.on("sync", function(state) {
if (state !== "PREPARED") {
return;
}
const room = client.getRoom(roomId);
let nameEmitCount = 0;
client.on("Room.name", function(rm) {
nameEmitCount += 1;
});
httpBackend.flush("/events", 1).done(function() {
httpBackend.flush("/sync", 1).done(function() {
expect(nameEmitCount).toEqual(1);
expect(room.name).toEqual("Room 2");
// do another round
eventData.chunk = [
utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 3"
}
})
];
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.flush("/events", 1).done(function() {
const thirdRoomNameEvent = utils.mkEvent({
user: userId, room: roomId, type: "m.room.name", content: {
name: "Room 3",
},
});
thirdRoomNameEvent.__prev_event = secondRoomNameEvent;
setNextSyncData([thirdRoomNameEvent]);
httpBackend.when("GET", "/sync").respond(200, NEXT_SYNC_DATA);
httpBackend.flush("/sync", 1).done(function() {
expect(nameEmitCount).toEqual(2);
expect(room.name).toEqual("Room 3");
done();
});
});
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should set the right room members", function(done) {
var userC = "@cee:bar";
var userD = "@dee:bar";
eventData.chunk = [
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,
}),
];
client.on("syncComplete", function() {
var room = client.getRoom(roomId);
httpBackend.flush("/events", 1).done(function() {
eventData[0].__prev_event = null;
eventData[1].__prev_event = null;
setNextSyncData(eventData);
client.on("sync", function(state) {
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);
});
httpBackend.flush("/sync", 1);
});
});
describe("gappy sync", function() {
it("should copy the last known state to the new timeline", function(done) {
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;
}
const room = client.getRoom(roomId);
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).done(function() {
expect(room.timeline.length).toEqual(1);
expect(room.timeline[0].event).toEqual(eventData[0]);
expect(room.currentState.getMembers().length).toEqual(2);
expect(room.currentState.getMember(userId).name).toEqual(userName);
expect(room.currentState.getMember(userId).membership).toEqual(
"join",
);
expect(room.currentState.getMember(otherUserId).name).toEqual("Bob");
expect(room.currentState.getMember(otherUserId).membership).toEqual(
"join",
);
done();
});
});
httpBackend.flush("/initialSync", 1);
httpBackend.flush("/sync", 1);
});
it("should emit a 'Room.timelineReset' event", function(done) {
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;
}
const room = client.getRoom(roomId);
let emitCount = 0;
client.on("Room.timelineReset", function(emitRoom) {
expect(emitRoom).toEqual(room);
emitCount++;
});
httpBackend.flush("/messages", 1);
httpBackend.flush("/sync", 1).done(function() {
expect(emitCount).toEqual(1);
done();
});
});
httpBackend.flush("/sync", 1);
});
});
});
+567 -160
View File
@@ -1,46 +1,50 @@
"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");
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";
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" });
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
});
describe("startClient", function() {
var initialSync = {
end: "s_5_3",
presence: [],
rooms: []
};
var eventData = {
start: "s_5_3",
end: "e_6_7",
chunk: []
const syncData = {
next_batch: "batch_token",
rooms: {},
presence: {},
};
it("should start with /initialSync then move onto /events.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
it("should /sync after /pushrules and /filter.", function(done) {
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
@@ -49,12 +53,12 @@ describe("MatrixClient syncing", function() {
});
});
it("should pass the 'end' token from /initialSync to the from= param " +
" of /events", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").check(function(req) {
expect(req.queryParams.from).toEqual(initialSync.end);
}).respond(200, eventData);
it("should pass the 'next_batch' token from /sync to the since= param " +
" of the next /sync", function(done) {
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").check(function(req) {
expect(req.queryParams.since).toEqual(syncData.next_batch);
}).respond(200, syncData);
client.startClient();
@@ -64,165 +68,314 @@ describe("MatrixClient syncing", function() {
});
});
describe("resolving invites to profile info", function() {
const syncData = {
next_batch: "s_5_3",
presence: {
events: [],
},
rooms: {
join: {
},
},
};
beforeEach(function() {
syncData.presence.events = [];
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
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,
}),
);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/profile/" + encodeURIComponent(userC)).respond(
200, {
avatar_url: "mxc://flibble/wibble",
displayname: "The Boss",
},
);
client.startClient({
resolveInvitesToProfiles: true,
});
httpBackend.flush().done(function() {
const member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual("The Boss");
expect(
member.getAvatarUrl("home.server.url", null, null, null, false),
).toBeDefined();
done();
});
});
it("should use cached values from m.presence wherever possible", function(done) {
syncData.presence.events = [
utils.mkPresence({
user: userC, presence: "online", name: "The Ghost",
}),
];
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC,
}),
);
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient({
resolveInvitesToProfiles: true,
});
httpBackend.flush().done(function() {
const member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual("The Ghost");
done();
});
});
it("should result in events on the room member firing", function(done) {
syncData.presence.events = [
utils.mkPresence({
user: userC, presence: "online", name: "The Ghost",
}),
];
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC,
}),
);
httpBackend.when("GET", "/sync").respond(200, syncData);
let latestFiredName = null;
client.on("RoomMember.name", function(event, m) {
if (m.userId === userC && m.roomId === roomOne) {
latestFiredName = m.name;
}
});
client.startClient({
resolveInvitesToProfiles: true,
});
httpBackend.flush().done(function() {
expect(latestFiredName).toEqual("The Ghost");
done();
});
});
it("should no-op if resolveInvitesToProfiles is not set", function(done) {
syncData.rooms.join[roomOne].state.events.push(
utils.mkMembership({
room: roomOne, mship: "invite", user: userC,
}),
);
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush().done(function() {
const member = client.getRoom(roomOne).getMember(userC);
expect(member.name).toEqual(userC);
expect(
member.getAvatarUrl("home.server.url", null, null, null, false),
).toBeNull();
done();
});
});
});
describe("users", function() {
var userA = "@alice:bar";
var userB = "@bob:bar";
var userC = "@claire:bar";
var initialSync = {
end: "s_5_3",
presence: [
utils.mkPresence({
user: userA, presence: "online"
}),
utils.mkPresence({
user: userB, presence: "unavailable"
})
],
rooms: []
};
var eventData = {
start: "s_5_3",
end: "e_6_7",
chunk: [
// existing user change
utils.mkPresence({
user: userA, presence: "offline"
}),
// new user C
utils.mkPresence({
user: userC, presence: "online"
})
]
const syncData = {
next_batch: "nb",
presence: {
events: [
utils.mkPresence({
user: userA, presence: "online",
}),
utils.mkPresence({
user: userB, presence: "unavailable",
}),
],
},
};
it("should create users for presence events from /initialSync and /events",
it("should create users for presence events from /sync",
function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush().done(function() {
expect(client.getUser(userA).presence).toEqual("offline");
expect(client.getUser(userA).presence).toEqual("online");
expect(client.getUser(userB).presence).toEqual("unavailable");
expect(client.getUser(userC).presence).toEqual("online");
done();
});
});
});
describe("room state", function() {
var roomOne = "!foo:localhost";
var roomTwo = "!bar:localhost";
var msgText = "some text here";
var otherDisplayName = "Bob Smith";
var initialSync = {
end: "s_5_3",
presence: [],
rooms: [
{
membership: "join",
room_id: roomOne,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello"
})
]
},
state: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name"
}
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId
}
})
]
const msgText = "some text here";
const otherDisplayName = "Bob Smith";
const syncData = {
rooms: {
join: {
},
{
membership: "join",
room_id: roomTwo,
messages: {
start: "f_1_1",
end: "f_2_2",
chunk: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hiii"
})
]
},
state: [
utils.mkMembership({
room: roomTwo, mship: "join", user: otherUserId,
name: otherDisplayName
}),
utils.mkMembership({
room: roomTwo, mship: "join", user: selfUserId
}),
utils.mkEvent({
type: "m.room.create", room: roomTwo, user: selfUserId,
content: {
creator: selfUserId
}
})
]
}
]
},
};
var eventData = {
start: "s_5_3",
end: "e_6_7",
chunk: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: selfUserId,
content: { name: "A new room name" }
}),
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: msgText
}),
utils.mkEvent({
type: "m.typing", room: roomTwo,
content: { user_ids: [otherUserId] }
})
]
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
],
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name",
},
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
};
syncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hiii",
}),
],
},
state: {
events: [
utils.mkMembership({
room: roomTwo, mship: "join", user: otherUserId,
name: otherDisplayName,
}),
utils.mkMembership({
room: roomTwo, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomTwo, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
};
const nextSyncData = {
rooms: {
join: {
},
},
};
nextSyncData.rooms.join[roomOne] = {
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: selfUserId,
content: { name: "A new room name" },
}),
],
},
};
nextSyncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: msgText,
}),
],
},
ephemeral: {
events: [
utils.mkEvent({
type: "m.typing", room: roomTwo,
content: { user_ids: [otherUserId] },
}),
],
},
};
it("should continually recalculate the right room name.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
httpBackend.flush().done(function() {
var room = client.getRoom(roomOne);
const room = client.getRoom(roomOne);
// should have clobbered the name to the one from /events
expect(room.name).toEqual(eventData.chunk[0].content.name);
expect(room.name).toEqual(
nextSyncData.rooms.join[roomOne].state.events[0].content.name,
);
done();
});
});
it("should store the right events in the timeline.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
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);
@@ -231,12 +384,12 @@ describe("MatrixClient syncing", function() {
});
it("should set the right room name.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
httpBackend.flush().done(function() {
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();
@@ -244,14 +397,14 @@ describe("MatrixClient syncing", function() {
});
it("should set the right user's typing flag.", function(done) {
httpBackend.when("GET", "/initialSync").respond(200, initialSync);
httpBackend.when("GET", "/events").respond(200, eventData);
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
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);
@@ -270,6 +423,182 @@ describe("MatrixClient syncing", function() {
});
});
describe("timeline", function() {
beforeEach(function() {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
],
prev_batch: "pagTok",
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush();
});
it("should set the back-pagination token on new rooms", function(done) {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "roomtwo",
}),
],
prev_batch: "roomtwotok",
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
httpBackend.flush().then(function() {
const room = client.getRoom(roomTwo);
const tok = room.getLiveTimeline()
.getPaginationToken(EventTimeline.BACKWARDS);
expect(tok).toEqual("roomtwotok");
done();
}).catch(utils.failTest).done();
});
it("should set the back-pagination token on gappy syncs", function(done) {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world",
}),
],
limited: true,
prev_batch: "newerTok",
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
let resetCallCount = 0;
// the token should be set *before* timelineReset is emitted
client.on("Room.timelineReset", function(room) {
resetCallCount++;
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(0);
const tok = tl.getPaginationToken(EventTimeline.BACKWARDS);
expect(tok).toEqual("newerTok");
});
httpBackend.flush().then(function() {
const room = client.getRoom(roomOne);
const tl = room.getLiveTimeline();
expect(tl.getEvents().length).toEqual(1);
expect(resetCallCount).toEqual(1);
done();
}).catch(utils.failTest).done();
});
});
describe("receipts", function() {
const syncData = {
rooms: {
join: {
},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
}),
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world",
}),
],
},
state: {
events: [
utils.mkEvent({
type: "m.room.name", room: roomOne, user: otherUserId,
content: {
name: "Old room name",
},
}),
utils.mkMembership({
room: roomOne, mship: "join", user: otherUserId,
}),
utils.mkMembership({
room: roomOne, mship: "join", user: selfUserId,
}),
utils.mkEvent({
type: "m.room.create", room: roomOne, user: selfUserId,
content: {
creator: selfUserId,
},
}),
],
},
};
beforeEach(function() {
syncData.rooms.join[roomOne].ephemeral = {
events: [],
};
});
it("should sync receipts from /sync.", function(done) {
const ackEvent = syncData.rooms.join[roomOne].timeline.events[0];
const receipt = {};
receipt[ackEvent.event_id] = {
"m.read": {},
};
receipt[ackEvent.event_id]["m.read"][userC] = {
ts: 176592842636,
};
syncData.rooms.join[roomOne].ephemeral.events = [{
content: receipt,
room_id: roomOne,
type: "m.receipt",
}];
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
httpBackend.flush().done(function() {
const room = client.getRoom(roomOne);
expect(room.getReceiptsForEvent(new MatrixEvent(ackEvent))).toEqual([{
type: "m.read",
userId: userC,
data: {
ts: 176592842636,
},
}]);
done();
});
});
});
describe("of a room", function() {
xit("should sync when a join event (which changes state) for the user" +
" arrives down the event stream (e.g. join from another device)", function() {
@@ -280,4 +609,82 @@ describe("MatrixClient syncing", function() {
});
});
describe("syncLeftRooms", function() {
beforeEach(function(done) {
client.startClient();
httpBackend.flush().then(function() {
// the /sync call from syncLeftRooms ends up in the request
// queue behind the call from the running client; add a response
// to flush the client's one out.
httpBackend.when("GET", "/sync").respond(200, {});
done();
});
});
it("should create and use an appropriate filter", function(done) {
httpBackend.when("POST", "/filter").check(function(req) {
expect(req.data).toEqual({
room: { timeline: {limit: 1},
include_leave: true }});
}).respond(200, { filter_id: "another_id" });
httpBackend.when("GET", "/sync").check(function(req) {
expect(req.queryParams.filter).toEqual("another_id");
done();
}).respond(200, {});
client.syncLeftRooms();
// first flush the filter request; this will make syncLeftRooms
// make its /sync call
httpBackend.flush("/filter").then(function() {
// flush the syncs
return httpBackend.flush();
}).catch(utils.failTest);
});
it("should set the back-pagination token on left rooms", function(done) {
const syncData = {
next_batch: "batch_token",
rooms: {
leave: {},
},
};
syncData.rooms.leave[roomTwo] = {
timeline: {
events: [
utils.mkMessage({
room: roomTwo, user: otherUserId, msg: "hello",
}),
],
prev_batch: "pagTok",
},
};
httpBackend.when("POST", "/filter").respond(200, {
filter_id: "another_id",
});
httpBackend.when("GET", "/sync").respond(200, syncData);
client.syncLeftRooms().then(function() {
const room = client.getRoom(roomTwo);
const tok = room.getLiveTimeline().getPaginationToken(
EventTimeline.BACKWARDS);
expect(tok).toEqual("pagTok");
done();
}).catch(utils.failTest).done();
// first flush the filter request; this will make syncLeftRooms
// make its /sync call
httpBackend.flush("/filter").then(function() {
return httpBackend.flush();
}).catch(utils.failTest);
});
});
});
File diff suppressed because it is too large Load Diff
+117 -36
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,12 +8,26 @@ 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;
self.requests.push(realReq);
const req = new Request(opts, callback);
console.log("HTTP backend received request: %s", req);
self.requests.push(req);
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);
req.callback("aborted");
}
};
return {
abort: abort,
};
};
}
HttpBackend.prototype = {
@@ -24,17 +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;
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.
@@ -43,12 +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 {
} else if (flushed === 0 && !triedWaiting) {
// we may not have made the request yet, wait a generous amount of
// time before giving up.
setTimeout(tryFlush, 5);
triedWaiting = true;
} else {
console.log(" no more flushes. [%s]", path);
defer.resolve();
}
@@ -65,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) ===
@@ -94,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;
}
@@ -114,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,
);
},
@@ -125,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,
);
},
@@ -139,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.
@@ -175,10 +208,10 @@ Request.prototype = {
this.response = {
response: {
statusCode: code,
headers: {}
headers: {},
},
body: data,
err: null
err: null,
};
},
@@ -191,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;
},
};
/**
+84 -26
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.
@@ -48,7 +47,7 @@ module.exports.mock = function(constr, name) {
* @param {Object} opts Values for the event.
* @param {string} opts.type The event.type
* @param {string} opts.room The event.room_id
* @param {string} opts.user The event.user_id
* @param {string} opts.sender The event.sender
* @param {string} opts.skey Optional. The state key (auto inserts empty string)
* @param {Object} opts.content The event.content
* @param {boolean} opts.event True to make a MatrixEvent.
@@ -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,
user_id: opts.user,
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,16 +83,16 @@ 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
content: {
user_id: opts.user,
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;
};
@@ -104,8 +102,8 @@ module.exports.mkPresence = function(opts) {
* @param {Object} opts Values for the membership.
* @param {string} opts.room The room ID for the event.
* @param {string} opts.mship The content.membership for the event.
* @param {string} opts.user The user ID for the event.
* @param {string} opts.skey The other user ID for the event if applicable
* @param {string} opts.sender The sender user ID for the event.
* @param {string} opts.skey The target user ID for the event if applicable
* e.g. for invites/bans.
* @param {string} opts.name The content.displayname for the event.
* @param {string} opts.url The content.avatar_url for the event.
@@ -115,16 +113,20 @@ module.exports.mkPresence = function(opts) {
module.exports.mkMembership = function(opts) {
opts.type = "m.room.member";
if (!opts.skey) {
opts.skey = opts.user;
opts.skey = opts.sender || opts.user;
}
if (!opts.mship) {
throw new Error("Missing .mship => " + JSON.stringify(opts));
}
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,63 @@ module.exports.mkMessage = function(opts) {
}
opts.content = {
msgtype: "m.text",
body: opts.msg
body: opts.msg,
};
return module.exports.mkEvent(opts);
};
/**
* make the test fail, with the given exception
*
* <p>This is useful for use with integration tests which use asyncronous
* methods: it can be added as a 'catch' handler in a promise chain.
*
* @param {Error} err exception to be reported
*
* @deprecated
* It turns out there are easier ways of doing this. Just use nodeify():
*
* it("should not throw", function(done) {
* asynchronousMethod().then(function() {
* // some tests
* }).nodeify(done);
* });
*
* @example
* it("should not throw", function(done) {
* asynchronousMethod().then(function() {
* // some tests
* }).catch(utils.failTest).done(done);
* });
*/
module.exports.failTest = function(err) {
expect(true).toBe(false, "Testfunc threw: " + err.stack);
};
/**
* A mock implementation of webstorage
*
* @constructor
*/
module.exports.MockStorageApi = function() {
this.data = {};
};
module.exports.MockStorageApi.prototype = {
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];
},
};
+92
View File
@@ -0,0 +1,92 @@
"use strict";
const ContentRepo = require("../../lib/content-repo");
const testUtils = require("../test-utils");
describe("ContentRepo", function() {
const baseUrl = "https://my.home.server";
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe("getHttpUriForMxc", function() {
it("should do nothing to HTTP URLs when allowing direct links", function() {
const httpUrl = "http://example.com/image.jpeg";
expect(
ContentRepo.getHttpUriForMxc(
baseUrl, httpUrl, undefined, undefined, undefined, true,
),
).toEqual(httpUrl);
});
it("should return the empty string HTTP URLs by default", function() {
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() {
const mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v1/download/server.name/resourceid",
);
});
it("should return the empty string for null input", function() {
expect(ContentRepo.getHttpUriForMxc(null)).toEqual("");
});
it("should return a thumbnail URL if a width/height/resize is specified",
function() {
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",
);
});
it("should put fragments from mxc:// URIs after any query parameters",
function() {
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",
);
});
it("should put fragments from mxc:// URIs at the end of the HTTP URI",
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v1/download/server.name/resourceid#automade",
);
});
});
describe("getIdenticonUri", function() {
it("should do nothing for null input", function() {
expect(ContentRepo.getIdenticonUri(null)).toEqual(null);
});
it("should set w/h by default to 96", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar")).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foobar" +
"?width=96&height=96",
);
});
it("should be able to set custom w/h", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foobar" +
"?width=32&height=64",
);
});
it("should URL encode the identicon string", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foo%23bar" +
"?width=32&height=64",
);
});
});
});
+14
View File
@@ -0,0 +1,14 @@
"use strict";
const Crypto = require("../../lib/crypto");
const sdk = require("../..");
describe("Crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
it("Crypto exposes the correct olm library version", function() {
expect(Crypto.getOlmVersion()[0]).toEqual(2);
});
});
+376
View File
@@ -0,0 +1,376 @@
"use strict";
const sdk = require("../..");
const EventTimeline = sdk.EventTimeline;
const utils = require("../test-utils");
function mockRoomStates(timeline) {
timeline._startState = utils.mock(sdk.RoomState, "startState");
timeline._endState = utils.mock(sdk.RoomState, "endState");
}
describe("EventTimeline", function() {
const roomId = "!foo:bar";
const userA = "@alice:bar";
const userB = "@bertha:bar";
let timeline;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
// XXX: this is a horrid hack; should use sinon or something instead to mock
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() {
const v = timeline.getRoomId();
expect(v).toEqual(roomId);
});
});
describe("initialiseState", function() {
beforeEach(function() {
mockRoomStates(timeline);
});
it("should copy state events to start and end state", function() {
const events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA,
event: true,
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB,
event: true,
content: { name: "New room" },
}),
];
timeline.initialiseState(events);
expect(timeline._startState.setStateEvents).toHaveBeenCalledWith(
events,
);
expect(timeline._endState.setStateEvents).toHaveBeenCalledWith(
events,
);
});
it("should raise an exception if called after events are added", function() {
const event =
utils.mkMessage({
room: roomId, user: userA, msg: "Adam stole the plushies",
event: true,
});
const state = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA,
event: true,
}),
];
expect(function() {
timeline.initialiseState(state);
}).not.toThrow();
timeline.addEvent(event, false);
expect(function() {
timeline.initialiseState(state);
}).toThrow();
});
});
describe("paginationTokens", function() {
it("pagination tokens should start null", function() {
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toBe(null);
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toBe(null);
});
it("setPaginationToken should set token", function() {
timeline.setPaginationToken("back", EventTimeline.BACKWARDS);
timeline.setPaginationToken("fwd", EventTimeline.FORWARDS);
expect(timeline.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("back");
expect(timeline.getPaginationToken(EventTimeline.FORWARDS)).toEqual("fwd");
});
});
describe("neighbouringTimelines", function() {
it("neighbouring timelines should start null", function() {
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(null);
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(null);
});
it("setNeighbouringTimeline should set neighbour", function() {
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);
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)).toBe(next);
});
it("setNeighbouringTimeline should throw if called twice", function() {
const prev = {a: "a"};
const next = {b: "b"};
expect(function() {
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
}).not.toThrow();
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS))
.toBe(prev);
expect(function() {
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
}).toThrow();
expect(function() {
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
}).not.toThrow();
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS))
.toBe(next);
expect(function() {
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
}).toThrow();
});
});
describe("addEvent", function() {
beforeEach(function() {
mockRoomStates(timeline);
});
const events = [
utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry",
event: true,
}),
utils.mkMessage({
room: roomId, user: userB, msg: "nom nom nom",
event: true,
}),
];
it("should be able to add events to the end", function() {
timeline.addEvent(events[0], false);
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], false);
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getEvents()[0]).toEqual(events[0]);
expect(timeline.getEvents()[1]).toEqual(events[1]);
});
it("should be able to add events to the start", function() {
timeline.addEvent(events[0], true);
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], true);
expect(timeline.getBaseIndex()).toEqual(initialIndex + 1);
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getEvents()[0]).toEqual(events[1]);
expect(timeline.getEvents()[1]).toEqual(events[0]);
});
it("should set event.sender for new and old events", function() {
const sentinel = {
userId: userA,
membership: "join",
name: "Alice",
};
const oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice",
};
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
.andCallFake(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
const newEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "New Room Name" },
});
const oldEv = utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, event: true,
content: { name: "Old Room Name" },
});
timeline.addEvent(newEv, false);
expect(newEv.sender).toEqual(sentinel);
timeline.addEvent(oldEv, true);
expect(oldEv.sender).toEqual(oldSentinel);
});
it("should set event.target for new and old m.room.member events",
function() {
const sentinel = {
userId: userA,
membership: "join",
name: "Alice",
};
const oldSentinel = {
userId: userA,
membership: "join",
name: "Old Alice",
};
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
.andCallFake(function(uid) {
if (uid === userA) {
return sentinel;
}
return null;
});
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
.andCallFake(function(uid) {
if (uid === userA) {
return oldSentinel;
}
return null;
});
const newEv = utils.mkMembership({
room: roomId, mship: "invite", 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);
timeline.addEvent(oldEv, true);
expect(oldEv.target).toEqual(oldSentinel);
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for new events", function() {
const events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room",
},
}),
];
timeline.addEvent(events[0], false);
timeline.addEvent(events[1], false);
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
toHaveBeenCalledWith([events[0]]);
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
toHaveBeenCalledWith([events[1]]);
expect(events[0].forwardLooking).toBe(true);
expect(events[1].forwardLooking).toBe(true);
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
not.toHaveBeenCalled();
});
it("should call setStateEvents on the right RoomState with the right " +
"forwardLooking value for old events", function() {
const events = [
utils.mkMembership({
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
}),
utils.mkEvent({
type: "m.room.name", room: roomId, user: userB, event: true,
content: {
name: "New room",
},
}),
];
timeline.addEvent(events[0], true);
timeline.addEvent(events[1], true);
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
toHaveBeenCalledWith([events[0]]);
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
toHaveBeenCalledWith([events[1]]);
expect(events[0].forwardLooking).toBe(false);
expect(events[1].forwardLooking).toBe(false);
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
not.toHaveBeenCalled();
});
});
describe("removeEvent", function() {
const events = [
utils.mkMessage({
room: roomId, user: userA, msg: "hungry hungry hungry",
event: true,
}),
utils.mkMessage({
room: roomId, user: userB, msg: "nom nom nom",
event: true,
}),
utils.mkMessage({
room: roomId, user: userB, msg: "piiie",
event: true,
}),
];
it("should remove events", function() {
timeline.addEvent(events[0], false);
timeline.addEvent(events[1], false);
expect(timeline.getEvents().length).toEqual(2);
let ev = timeline.removeEvent(events[0].getId());
expect(ev).toBe(events[0]);
expect(timeline.getEvents().length).toEqual(1);
ev = timeline.removeEvent(events[1].getId());
expect(ev).toBe(events[1]);
expect(timeline.getEvents().length).toEqual(0);
});
it("should update baseIndex", function() {
timeline.addEvent(events[0], false);
timeline.addEvent(events[1], true);
timeline.addEvent(events[2], false);
expect(timeline.getEvents().length).toEqual(3);
expect(timeline.getBaseIndex()).toEqual(1);
timeline.removeEvent(events[2].getId());
expect(timeline.getEvents().length).toEqual(2);
expect(timeline.getBaseIndex()).toEqual(1);
timeline.removeEvent(events[1].getId());
expect(timeline.getEvents().length).toEqual(1);
expect(timeline.getBaseIndex()).toEqual(0);
});
// this is basically https://github.com/vector-im/vector-web/issues/937
// - removing the last event got baseIndex into such a state that
// further addEvent(ev, false) calls made the index increase.
it("should not make baseIndex assplode when removing the last event",
function() {
timeline.addEvent(events[0], true);
timeline.removeEvent(events[0].getId());
const initialIndex = timeline.getBaseIndex();
timeline.addEvent(events[1], false);
timeline.addEvent(events[2], false);
expect(timeline.getBaseIndex()).toEqual(initialIndex);
expect(timeline.getEvents().length).toEqual(2);
});
});
});
+50
View File
@@ -0,0 +1,50 @@
"use strict";
const sdk = require("../..");
const Filter = sdk.Filter;
const utils = require("../test-utils");
describe("Filter", function() {
const filterId = "f1lt3ring15g00d4ursoul";
const userId = "@sir_arthur_david:humming.tiger";
let filter;
beforeEach(function() {
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() {
const definition = {
event_fields: ["type", "content"],
};
const f = Filter.fromJson(userId, filterId, definition);
expect(f.getDefinition()).toEqual(definition);
expect(f.userId).toEqual(userId);
expect(f.filterId).toEqual(filterId);
});
});
describe("setTimelineLimit", function() {
it("should set room.timeline.limit of the filter definition", function() {
filter.setTimelineLimit(10);
expect(filter.getDefinition()).toEqual({
room: {
timeline: {
limit: 10,
},
},
});
});
});
describe("setDefinition/getDefinition", function() {
it("should set and get the filter body", function() {
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);
});
});
+518
View File
@@ -0,0 +1,518 @@
"use strict";
const q = require("q");
const sdk = require("../..");
const MatrixClient = sdk.MatrixClient;
const utils = require("../test-utils");
describe("MatrixClient", function() {
const userId = "@alice:bar";
const identityServerUrl = "https://identity.server";
const identityServerDomain = "identity.server";
let client;
let store;
let scheduler;
const KEEP_ALIVE_PATH = "/_matrix/client/versions";
const PUSH_RULES_RESPONSE = {
method: "GET",
path: "/pushrules/",
data: {},
};
const FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter";
const FILTER_RESPONSE = {
method: "POST",
path: FILTER_PATH,
data: { filter_id: "f1lt3r" },
};
const SYNC_DATA = {
next_batch: "s_5_3",
presence: { events: [] },
rooms: {},
};
const SYNC_RESPONSE = {
method: "GET",
path: "/sync",
data: SYNC_DATA,
};
let httpLookups = [
// items are objects which look like:
// {
// method: "GET",
// path: "/initialSync",
// data: {},
// error: { errcode: M_FORBIDDEN } // if present will reject promise,
// expectBody: {} // additional expects on the body
// expectQueryParams: {} // additional expects on query params
// thenCall: function(){} // function to call *AFTER* returning response.
// }
// items are popped off when processed and block if no items left.
];
let acceptKeepalives;
let pendingLookup = null;
function httpReq(cb, method, path, qp, data, prefix) {
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
return q();
}
const next = httpLookups.shift();
const logLine = (
"MatrixClient[UT] RECV " + method + " " + path + " " +
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
);
console.log(logLine);
if (!next) { // no more things to return
if (pendingLookup) {
if (pendingLookup.method === method && pendingLookup.path === path) {
return pendingLookup.promise;
}
// >1 pending thing, and they are different, whine.
expect(false).toBe(
true, ">1 pending request. You should probably handle them. " +
"PENDING: " + JSON.stringify(pendingLookup) + " JUST GOT: " +
method + " " + path,
);
}
pendingLookup = {
promise: q.defer().promise,
method: method,
path: path,
};
return pendingLookup.promise;
}
if (next.path === path && next.method === method) {
console.log(
"MatrixClient[UT] Matched. Returning " +
(next.error ? "BAD" : "GOOD") + " response",
);
if (next.expectBody) {
expect(next.expectBody).toEqual(data);
}
if (next.expectQueryParams) {
Object.keys(next.expectQueryParams).forEach(function(k) {
expect(qp[k]).toEqual(next.expectQueryParams[k]);
});
}
if (next.thenCall) {
process.nextTick(next.thenCall, 0); // next tick so we return first.
}
if (next.error) {
return q.reject({
errcode: next.error.errcode,
httpStatus: next.error.httpStatus,
name: next.error.errcode,
message: "Expected testing error",
data: next.error,
});
}
return q(next.data);
}
expect(true).toBe(false, "Expected different request. " + logLine);
return q.defer().promise;
}
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
jasmine.Clock.useMock();
scheduler = jasmine.createSpyObj("scheduler", [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
]);
store = jasmine.createSpyObj("store", [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"setSyncToken", "storeEvents", "storeRoom", "storeUser",
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter",
]);
client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl,
accessToken: "my.access.token",
request: function() {}, // NOP
store: store,
scheduler: scheduler,
userId: userId,
});
// FIXME: We shouldn't be yanking _http like this.
client._http = jasmine.createSpyObj("httpApi", [
"authedRequest", "authedRequestWithPrefix", "getContentUri",
"request", "requestWithPrefix", "uploadContent",
]);
client._http.authedRequest.andCallFake(httpReq);
client._http.authedRequestWithPrefix.andCallFake(httpReq);
client._http.requestWithPrefix.andCallFake(httpReq);
client._http.request.andCallFake(httpReq);
// set reasonable working defaults
acceptKeepalives = true;
pendingLookup = null;
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
});
afterEach(function() {
// need to re-stub the requests with NOPs because there are no guarantees
// clients from previous tests will be GC'd before the next test. This
// means they may call /events and then fail an expect() which will fail
// a DIFFERENT test (pollution between tests!) - we return unresolved
// promises to stop the client from continuing to run.
client._http.authedRequest.andCallFake(function() {
return q.defer().promise;
});
client._http.authedRequestWithPrefix.andCallFake(function() {
return q.defer().promise;
});
});
it("should not POST /filter if a matching filter already exists", function(done) {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
const filterId = "ehfewf";
store.getFilterIdByName.andReturn(filterId);
const filter = new sdk.Filter(0, filterId);
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
store.getFilter.andReturn(filter);
client.startClient();
client.on("sync", function syncListener(state) {
if (state === "SYNCING") {
expect(httpLookups.length).toEqual(0);
client.removeListener("sync", syncListener);
done();
}
});
});
describe("getSyncState", function() {
it("should return null if the client isn't started", function() {
expect(client.getSyncState()).toBeNull();
});
it("should return the same sync state as emitted sync events", function(done) {
client.on("sync", function syncListener(state) {
expect(state).toEqual(client.getSyncState());
if (state === "SYNCING") {
client.removeListener("sync", syncListener);
done();
}
});
client.startClient();
});
});
describe("getOrCreateFilter", function() {
it("should POST createFilter if no id is present in localStorage", function() {
});
it("should use an existing filter if id is present in localStorage", function() {
});
it("should handle localStorage filterId missing from the server", function(done) {
function getFilterName(userId, suffix) {
// scope this on the user ID because people may login on many accounts
// and they all need to be stored!
return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : "");
}
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();
expect(client.retryImmediately()).toBe(false);
});
it("should work on /filter", function(done) {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
client.on("sync", function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(2);
expect(client.retryImmediately()).toBe(true);
jasmine.Clock.tick(1);
} else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
} else {
// unexpected state transition!
expect(state).toEqual(null);
}
});
client.startClient();
});
it("should work on /sync", function(done) {
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA,
});
client.on("sync", function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(1);
expect(client.retryImmediately()).toBe(
true, "retryImmediately returned false",
);
jasmine.Clock.tick(1);
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
jasmine.Clock.tick(10000);
} else if (state === "SYNCING" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
}
});
client.startClient();
});
it("should work on /pushrules", function(done) {
httpLookups = [];
httpLookups.push({
method: "GET", path: "/pushrules/", error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
client.on("sync", function syncListener(state) {
if (state === "ERROR" && httpLookups.length > 0) {
expect(httpLookups.length).toEqual(3);
expect(client.retryImmediately()).toBe(true);
jasmine.Clock.tick(1);
} else if (state === "PREPARED" && httpLookups.length === 0) {
client.removeListener("sync", syncListener);
done();
} else {
// unexpected state transition!
expect(state).toEqual(null);
}
});
client.startClient();
});
});
describe("emitted sync events", function() {
function syncChecker(expectedStates, done) {
return function syncListener(state, old) {
const expected = expectedStates.shift();
console.log(
"'sync' curr=%s old=%s EXPECT=%s", state, old, expected,
);
if (!expected) {
done();
return;
}
expect(state).toEqual(expected[0]);
expect(old).toEqual(expected[1]);
if (expectedStates.length === 0) {
client.removeListener("sync", syncListener);
done();
}
// standard retry time is 5 to 10 seconds
jasmine.Clock.tick(10000);
};
}
it("should transition null -> PREPARED after the first /sync", function(done) {
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) {
const expectedStates = [];
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push({
method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" },
});
expectedStates.push(["ERROR", null]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition ERROR -> PREPARED after /sync if prev failed",
function(done) {
const expectedStates = [];
acceptKeepalives = false;
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(FILTER_RESPONSE);
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH, data: {},
});
httpLookups.push({
method: "GET", path: "/sync", data: SYNC_DATA,
});
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["PREPARED", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition PREPARED -> SYNCING after /sync", function(done) {
const expectedStates = [];
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition SYNCING -> ERROR after a failed /sync", function(done) {
acceptKeepalives = false;
const expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
xit("should transition ERROR -> SYNCING after /sync if prev failed",
function(done) {
const expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
httpLookups.push(SYNC_RESPONSE);
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["ERROR", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition SYNCING -> SYNCING on subsequent /sync successes",
function(done) {
const expectedStates = [];
httpLookups.push(SYNC_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["SYNCING", "SYNCING"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
it("should transition ERROR -> ERROR if keepalive keeps failing", function(done) {
acceptKeepalives = false;
const expectedStates = [];
httpLookups.push({
method: "GET", path: "/sync", error: { errcode: "NONONONONO" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
httpLookups.push({
method: "GET", path: KEEP_ALIVE_PATH,
error: { errcode: "KEEPALIVE_FAIL" },
});
expectedStates.push(["PREPARED", null]);
expectedStates.push(["SYNCING", "PREPARED"]);
expectedStates.push(["RECONNECTING", "SYNCING"]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["ERROR", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
});
describe("inviteByEmail", function() {
const roomId = "!foo:bar";
it("should send an invite HTTP POST", function() {
httpLookups = [{
method: "POST",
path: "/rooms/!foo%3Abar/invite",
data: {},
expectBody: {
id_server: identityServerDomain,
medium: "email",
address: "alice@gmail.com",
},
}];
client.inviteByEmail(roomId, "alice@gmail.com");
expect(httpLookups.length).toEqual(0);
});
});
describe("guest rooms", function() {
it("should only do /sync calls (without filter/pushrules)", function(done) {
httpLookups = []; // no /pushrules or /filter
httpLookups.push({
method: "GET",
path: "/sync",
data: SYNC_DATA,
thenCall: function() {
done();
},
});
client.setGuest(true);
client.startClient();
});
xit("should be able to peek into a room using peekInRoom", function(done) {
});
});
});
+77 -78
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.event);
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.event);
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.event);
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.event);
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.event);
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.event);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
@@ -254,43 +253,43 @@ describe('NotificationService', function() {
it('should bing on a bing word.', function() {
testEvent.event.content.body = "I really like coffee";
var actions = pushProcessor.actionsForEvent(testEvent.event);
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.event);
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.event);
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.event);
let actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
testEvent.event.content.body = "Pong!";
actions = pushProcessor.actionsForEvent(testEvent.event);
actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
});
it('should bing on character range ([a-z]) bing words.', function() {
testEvent.event.content.body = "I ate 6 pies";
var actions = pushProcessor.actionsForEvent(testEvent.event);
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.event);
let actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(true);
testEvent.event.content.body = "bake";
actions = pushProcessor.actionsForEvent(testEvent.event);
actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(false);
});
@@ -298,7 +297,7 @@ describe('NotificationService', function() {
it('should gracefully handle bad input.', function() {
testEvent.event.content.body = { "foo": "bar" };
var actions = pushProcessor.actionsForEvent(testEvent.event);
const actions = pushProcessor.actionsForEvent(testEvent);
expect(actions.tweaks.highlight).toEqual(false);
});
});
+180
View File
@@ -0,0 +1,180 @@
"use strict";
const callbacks = require("../../lib/realtime-callbacks");
const testUtils = require("../test-utils.js");
describe("realtime-callbacks", function() {
const clock = jasmine.Clock;
let fakeDate;
function tick(millis) {
// make sure we tick the fakedate first, otherwise nothing will happen!
fakeDate += millis;
clock.tick(millis);
}
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
clock.useMock();
fakeDate = Date.now();
callbacks.setNow(function() {
return fakeDate;
});
});
afterEach(function() {
callbacks.setNow();
});
describe("setTimeout", function() {
it("should call the callback after the timeout", function() {
const callback = jasmine.createSpy();
callbacks.setTimeout(callback, 100);
expect(callback).not.toHaveBeenCalled();
tick(100);
expect(callback).toHaveBeenCalled();
});
it("should default to a zero timeout", function() {
const callback = jasmine.createSpy();
callbacks.setTimeout(callback);
expect(callback).not.toHaveBeenCalled();
tick(0);
expect(callback).toHaveBeenCalled();
});
it("should pass any parameters to the callback", function() {
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() {
const callback = jasmine.createSpy();
callback.andCallFake(function() {
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);
expect(callback).toHaveBeenCalled();
});
it("should handle timeouts of several seconds", function() {
const callback = jasmine.createSpy();
callbacks.setTimeout(callback, 2000);
expect(callback).not.toHaveBeenCalled();
for (let i = 0; i < 4; i++) {
tick(500);
}
expect(callback).toHaveBeenCalled();
});
it("should call multiple callbacks in the right order", function() {
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);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
expect(callback3).not.toHaveBeenCalled();
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
expect(callback3).toHaveBeenCalled();
});
it("should treat -ve timeouts the same as a zero timeout", function() {
const callback1 = jasmine.createSpy("callback1");
const callback2 = jasmine.createSpy("callback2");
// check that cb1 is called before cb2
callback1.andCallFake(function() {
expect(callback2).not.toHaveBeenCalled();
});
callbacks.setTimeout(callback1);
callbacks.setTimeout(callback2, -100);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
it("should not get confused by chained calls", function() {
const callback2 = jasmine.createSpy("callback2");
const callback1 = jasmine.createSpy("callback1");
callback1.andCallFake(function() {
callbacks.setTimeout(callback2, 0);
expect(callback2).not.toHaveBeenCalled();
});
callbacks.setTimeout(callback1);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
it("should be immune to exceptions", function() {
const callback1 = jasmine.createSpy("callback1");
callback1.andCallFake(function() {
throw new Error("prepare to die");
});
const callback2 = jasmine.createSpy("callback2");
callbacks.setTimeout(callback1, 0);
callbacks.setTimeout(callback2, 0);
expect(callback1).not.toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
tick(0);
expect(callback1).toHaveBeenCalled();
expect(callback2).toHaveBeenCalled();
});
});
describe("cancelTimeout", function() {
it("should cancel a pending timeout", function() {
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() {
const callback1 = jasmine.createSpy("callback1");
const callback2 = jasmine.createSpy("callback2");
callbacks.setTimeout(callback1, 100);
const k = callbacks.setTimeout(callback2, 200);
callbacks.clearTimeout(k);
tick(100);
expect(callback1).toHaveBeenCalled();
expect(callback2).not.toHaveBeenCalled();
tick(150);
expect(callback2).not.toHaveBeenCalled();
});
});
});
+112 -45
View File
@@ -1,23 +1,57 @@
"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() {
const hsUrl = "https://my.home.server";
it("should return the URL from m.room.member preferentially", function() {
member.events.member = utils.mkEvent({
event: true,
type: "m.room.member",
skey: userA,
room: roomId,
user: userA,
content: {
membership: "join",
avatar_url: "mxc://flibble/wibble",
},
});
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);
});
it("should return an identicon HTTP URL if allowDefault was set and there " +
"was no m.room.member event", function() {
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() {
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,
@@ -25,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);
@@ -42,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,
@@ -50,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;
@@ -68,26 +102,57 @@ describe("RoomMember", function() {
member.setPowerLevelEvent(event); // no-op
expect(emitCount).toEqual(1);
});
it("should honour power levels of zero.",
function() {
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@alice:bar": 0,
},
},
event: true,
});
let emitCount = 0;
// set the power level to something other than zero or we
// won't get an event
member.powerLevel = 1;
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember.userId).toEqual('@alice:bar');
expect(emitMember.powerLevel).toEqual(0);
expect(emitEvent).toEqual(event);
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(0);
expect(emitCount).toEqual(1);
});
});
describe("setTypingEvent", function() {
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);
@@ -100,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);
@@ -125,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'.",
@@ -153,21 +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);
@@ -179,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);
@@ -192,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);
@@ -203,8 +273,5 @@ describe("RoomMember", function() {
member.setMembershipEvent(joinEvent); // no-op
expect(emitCount).toEqual(1);
});
});
});
+233 -80
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,11 +272,164 @@ describe("RoomState", function() {
state.setTypingEvent(typingEvent);
expect(state.members[userA].setTypingEvent).toHaveBeenCalledWith(
typingEvent
typingEvent,
);
expect(state.members[userB].setTypingEvent).toHaveBeenCalledWith(
typingEvent
typingEvent,
);
});
});
describe("maySendStateEvent", function() {
it("should say non-joined members may not send state",
function() {
expect(state.maySendStateEvent(
'm.room.name', "@nobody:nowhere",
)).toEqual(false);
});
it("should say any member may send state with no power level event",
function() {
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
});
it("should say members with power >=50 may send state with power level event " +
"but no state default",
function() {
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;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
});
it("should obey state_default",
function() {
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;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
});
it("should honour explicit event power levels in the power_levels event",
function() {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
events: {
"m.room.other_thing": 76,
},
users_default: 10,
state_default: 50,
events_default: 25,
users: {
},
},
};
powerLevelEvent.content.users[userA] = 80;
powerLevelEvent.content.users[userB] = 50;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(true);
expect(state.maySendStateEvent('m.room.other_thing', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.other_thing', userB)).toEqual(false);
});
});
describe("maySendEvent", function() {
it("should say non-joined members may not send events",
function() {
expect(state.maySendEvent(
'm.room.message', "@nobody:nowhere",
)).toEqual(false);
expect(state.maySendMessage("@nobody:nowhere")).toEqual(false);
});
it("should say any member may send events with no power level event",
function() {
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendMessage(userA)).toEqual(true);
});
it("should obey events_default",
function() {
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;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendEvent('m.room.message', userB)).toEqual(false);
expect(state.maySendMessage(userA)).toEqual(true);
expect(state.maySendMessage(userB)).toEqual(false);
});
it("should honour explicit event power levels in the power_levels event",
function() {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
events: {
"m.room.other_thing": 33,
},
users_default: 10,
state_default: 50,
events_default: 25,
users: {
},
},
};
powerLevelEvent.content.users[userA] = 40;
powerLevelEvent.content.users[userB] = 30;
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendEvent('m.room.message', userB)).toEqual(true);
expect(state.maySendMessage(userA)).toEqual(true);
expect(state.maySendMessage(userB)).toEqual(true);
expect(state.maySendEvent('m.room.other_thing', userA)).toEqual(true);
expect(state.maySendEvent('m.room.other_thing', userB)).toEqual(false);
});
});
});
+1012 -296
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);
});
});
+475
View File
@@ -0,0 +1,475 @@
"use strict";
const q = require("q");
const sdk = require("../..");
const EventTimeline = sdk.EventTimeline;
const TimelineWindow = sdk.TimelineWindow;
const TimelineIndex = require("../../lib/timeline-window").TimelineIndex;
const utils = require("../test-utils");
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;
}
// XXX: this is a horrid hack
const timelineSet = { room: { roomId: ROOM_ID }};
timelineSet.room.getUnfilteredTimelineSet = function() {
return timelineSet;
};
const timeline = new EventTimeline(timelineSet);
// add the events after the baseIndex first
addEventsToTimeline(timeline, numEvents - baseIndex, false);
// then add those before the baseIndex
addEventsToTimeline(timeline, baseIndex, true);
expect(timeline.getBaseIndex()).toEqual(baseIndex);
return timeline;
}
function addEventsToTimeline(timeline, numEvents, atStart) {
for (let i = 0; i < numEvents; i++) {
timeline.addEvent(
utils.mkMessage({
room: ROOM_ID, user: USER_ID,
event: true,
}), atStart,
);
}
}
/*
* create a pair of linked timelines
*/
function createLinkedTimelines() {
const tl1 = createTimeline();
const tl2 = createTimeline();
tl1.setNeighbouringTimeline(tl2, EventTimeline.FORWARDS);
tl2.setNeighbouringTimeline(tl1, EventTimeline.BACKWARDS);
return [tl1, tl2];
}
describe("TimelineIndex", function() {
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe("minIndex", function() {
it("should return the min index relative to BaseIndex", function() {
const timelineIndex = new TimelineIndex(createTimeline(), 0);
expect(timelineIndex.minIndex()).toEqual(-1);
});
});
describe("maxIndex", function() {
it("should return the max index relative to BaseIndex", function() {
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() {
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() {
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() {
const timelines = createLinkedTimelines();
const tl1 = timelines[0];
const tl2 = timelines[1];
// initialise the index pointing at the end of the first timeline
const timelineIndex = new TimelineIndex(tl1, 2);
const result = timelineIndex.advance(1);
expect(result).toEqual(1);
expect(timelineIndex.timeline).toBe(tl2);
// we expect the index to be the zero (ie, the same as the
// BaseIndex), because the BaseIndex points at the second event,
// and we've advanced past the first.
expect(timelineIndex.index).toEqual(0);
});
it("should retreat into the previous timeline", function() {
const timelines = createLinkedTimelines();
const tl1 = timelines[0];
const tl2 = timelines[1];
// initialise the index pointing at the start of the second
// timeline
const timelineIndex = new TimelineIndex(tl2, -1);
const result = timelineIndex.advance(-1);
expect(result).toEqual(-1);
expect(timelineIndex.timeline).toBe(tl1);
expect(timelineIndex.index).toEqual(1);
});
});
describe("retreat", function() {
it("should retreat up to the start of the timeline", function() {
const timelineIndex = new TimelineIndex(createTimeline(), 0);
const result = timelineIndex.retreat(2);
expect(result).toEqual(1);
expect(timelineIndex.index).toEqual(-1);
});
});
});
describe("TimelineWindow", function() {
/**
* create a dummy eventTimelineSet and client, and a TimelineWindow
* attached to them.
*/
let timelineSet;
let client;
function createWindow(timeline, opts) {
timelineSet = {};
client = {};
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineSet0).toBe(timelineSet);
return q(timeline);
};
return new TimelineWindow(client, timelineSet, opts);
}
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
});
describe("load", function() {
it("should initialise from the live timeline", function(done) {
const liveTimeline = createTimeline();
const room = {};
room.getLiveTimeline = function() {
return liveTimeline;
};
const timelineWindow = new TimelineWindow(undefined, room);
timelineWindow.load(undefined, 2).then(function() {
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) {
const timeline = createTimeline();
const eventId = timeline.getEvents()[1].getId();
const timelineSet = {};
const client = {};
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineSet0).toBe(timelineSet);
expect(eventId0).toEqual(eventId);
return q(timeline);
};
const timelineWindow = new TimelineWindow(client, timelineSet);
timelineWindow.load(eventId, 3).then(function() {
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) {
const timeline = createTimeline();
timeline.setPaginationToken("toktok1", EventTimeline.BACKWARDS);
timeline.setPaginationToken("toktok2", EventTimeline.FORWARDS);
const eventId = timeline.getEvents()[1].getId();
const timelineSet = {};
const client = {};
const timelineWindow = new TimelineWindow(client, timelineSet);
client.getEventTimeline = function(timelineSet0, eventId0) {
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return q(timeline);
};
timelineWindow.load(eventId, 3).then(function() {
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
}).catch(utils.failTest).done(done);
});
});
describe("pagination", function() {
it("should be able to advance across the initial timeline",
function(done) {
const timeline = createTimeline();
const eventId = timeline.getEvents()[1].getId();
const timelineWindow = createWindow(timeline);
timelineWindow.load(eventId, 1).then(function() {
const expectedEvents = [timeline.getEvents()[1]];
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
const expectedEvents = timeline.getEvents().slice(1);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
}).catch(utils.failTest).done(done);
});
it("should advance into next timeline", function(done) {
const tls = createLinkedTimelines();
const eventId = tls[0].getEvents()[1].getId();
const timelineWindow = createWindow(tls[0], {windowLimit: 5});
timelineWindow.load(eventId, 3).then(function() {
const expectedEvents = tls[0].getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
const expectedEvents = tls[0].getEvents()
.concat(tls[1].getEvents().slice(0, 2));
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
// the windowLimit should have made us drop an event from
// tls[0]
const expectedEvents = tls[0].getEvents().slice(1)
.concat(tls[1].getEvents());
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
}).catch(utils.failTest).done(done);
});
it("should retreat into previous timeline", function(done) {
const tls = createLinkedTimelines();
const eventId = tls[1].getEvents()[1].getId();
const timelineWindow = createWindow(tls[1], {windowLimit: 5});
timelineWindow.load(eventId, 3).then(function() {
const expectedEvents = tls[1].getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
const expectedEvents = tls[0].getEvents().slice(1, 3)
.concat(tls[1].getEvents());
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
// the windowLimit should have made us drop an event from
// tls[1]
const expectedEvents = tls[0].getEvents()
.concat(tls[1].getEvents().slice(0, 2));
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(false);
}).catch(utils.failTest).done(done);
});
it("should make forward pagination requests", function(done) {
const timeline = createTimeline();
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
const timelineWindow = createWindow(timeline, {windowLimit: 5});
const eventId = timeline.getEvents()[1].getId();
client.paginateEventTimeline = function(timeline0, opts) {
expect(timeline0).toBe(timeline);
expect(opts.backwards).toBe(false);
expect(opts.limit).toEqual(2);
addEventsToTimeline(timeline, 3, false);
return q(true);
};
timelineWindow.load(eventId, 3).then(function() {
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
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) {
const timeline = createTimeline();
timeline.setPaginationToken("toktok", EventTimeline.BACKWARDS);
const timelineWindow = createWindow(timeline, {windowLimit: 5});
const eventId = timeline.getEvents()[1].getId();
client.paginateEventTimeline = function(timeline0, opts) {
expect(timeline0).toBe(timeline);
expect(opts.backwards).toBe(true);
expect(opts.limit).toEqual(2);
addEventsToTimeline(timeline, 3, true);
return q(true);
};
timelineWindow.load(eventId, 3).then(function() {
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(true);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(false);
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
}).then(function(success) {
expect(success).toBe(true);
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) {
const timeline = createTimeline();
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
const timelineWindow = createWindow(timeline, {windowLimit: 5});
const eventId = timeline.getEvents()[1].getId();
let paginateCount = 0;
client.paginateEventTimeline = function(timeline0, opts) {
expect(timeline0).toBe(timeline);
expect(opts.backwards).toBe(false);
expect(opts.limit).toEqual(2);
paginateCount += 1;
return q(true);
};
timelineWindow.load(eventId, 3).then(function() {
const expectedEvents = timeline.getEvents();
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
return timelineWindow.paginate(EventTimeline.FORWARDS, 2, true, 3);
}).then(function(success) {
expect(success).toBe(false);
expect(paginateCount).toEqual(3);
const expectedEvents = timeline.getEvents().slice(0, 3);
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
.toBe(false);
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
.toBe(true);
}).catch(utils.failTest).done(done);
});
});
});
+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;
});
+191 -35
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,23 +125,167 @@ 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() {
const assert = {
isTrue: function(x) {
expect(x).toBe(true);
},
isFalse: function(x) {
expect(x).toBe(false);
},
};
it("should handle primitives", function() {
assert.isTrue(utils.deepCompare(null, null));
assert.isFalse(utils.deepCompare(null, undefined));
assert.isTrue(utils.deepCompare("hi", "hi"));
assert.isTrue(utils.deepCompare(5, 5));
assert.isFalse(utils.deepCompare(5, 10));
});
it("should handle regexps", function() {
assert.isTrue(utils.deepCompare(/abc/, /abc/));
assert.isFalse(utils.deepCompare(/abc/, /123/));
const r = /abc/;
assert.isTrue(utils.deepCompare(r, r));
});
it("should handle dates", function() {
assert.isTrue(utils.deepCompare(new Date("2011-03-31"),
new Date("2011-03-31")));
assert.isFalse(utils.deepCompare(new Date("2011-03-31"),
new Date("1970-01-01")));
});
it("should handle arrays", function() {
assert.isTrue(utils.deepCompare([], []));
assert.isTrue(utils.deepCompare([1, 2], [1, 2]));
assert.isFalse(utils.deepCompare([1, 2], [2, 1]));
assert.isFalse(utils.deepCompare([1, 2], [1, 2, 3]));
});
it("should handle simple objects", function() {
assert.isTrue(utils.deepCompare({}, {}));
assert.isTrue(utils.deepCompare({a: 1, b: 2}, {a: 1, b: 2}));
assert.isTrue(utils.deepCompare({a: 1, b: 2}, {b: 2, a: 1}));
assert.isFalse(utils.deepCompare({a: 1, b: 2}, {a: 1, b: 3}));
assert.isTrue(utils.deepCompare({1: {name: "mhc", age: 28},
2: {name: "arb", age: 26}},
{1: {name: "mhc", age: 28},
2: {name: "arb", age: 26}}));
assert.isFalse(utils.deepCompare({1: {name: "mhc", age: 28},
2: {name: "arb", age: 26}},
{1: {name: "mhc", age: 28},
2: {name: "arb", age: 27}}));
assert.isFalse(utils.deepCompare({}, null));
assert.isFalse(utils.deepCompare({}, undefined));
});
it("should handle functions", function() {
// no two different function is equal really, they capture their
// context variables so even if they have same toString(), they
// won't have same functionality
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 } }));
assert.isFalse(utils.deepCompare({ a: { b: func } }, { a: { b: func2 } }));
});
});
describe("extend", function() {
const SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
it("should extend", function() {
const target = {
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
};
const merged = {
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
"newprop": "new",
};
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(sourceOrig);
});
it("should ignore null", function() {
const target = {
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
};
const merged = {
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
"newprop": "new",
};
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(sourceOrig);
});
it("should handle properties created with defineProperties", function() {
const source = Object.defineProperties({}, {
"enumerableProp": {
get: function() {
return true;
},
enumerable: true,
},
"nonenumerableProp": {
get: function() {
return true;
},
},
});
const target = {};
utils.extend(target, source);
expect(target.enumerableProp).toBe(true);
expect(target.nonenumerableProp).toBe(undefined);
});
});
});
-609
View File
@@ -1,609 +0,0 @@
"use strict";
var sdk = require("../..");
var WebStorageStore = sdk.WebStorageStore;
var Room = sdk.Room;
var User = sdk.User;
var utils = require("../test-utils");
function MockStorageApi() {
this.data = {};
this.keys = [];
this.length = 0;
}
MockStorageApi.prototype = {
setItem: function(k, v) {
this.data[k] = v;
this._recalc();
},
getItem: function(k) {
return this.data[k] || null;
},
removeItem: function(k) {
delete this.data[k];
this._recalc();
},
key: function(index) {
return this.keys[index];
},
_recalc: function() {
var keys = [];
for (var k in this.data) {
if (!this.data.hasOwnProperty(k)) { continue; }
keys.push(k);
}
this.keys = keys;
this.length = keys.length;
}
};
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));
}
+1236
View File
File diff suppressed because it is too large Load Diff
+3276
View File
File diff suppressed because it is too large Load Diff
+109
View File
@@ -0,0 +1,109 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module content-repo
*/
const utils = require("./utils");
/** Content Repo utility functions */
module.exports = {
/**
* Get the HTTP URL for an MXC URI.
* @param {string} baseUrl The base homeserver url which has a content repo.
* @param {string} mxc The mxc:// URI.
* @param {Number} width The desired width of the thumbnail.
* @param {Number} height The desired height of the thumbnail.
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @param {Boolean} allowDirectLinks If true, return any non-mxc URLs
* directly. Fetching such URLs will leak information about the user to
* anyone they share a room with. If false, will return the emptry string
* for such URLs.
* @return {string} The complete URL to the content.
*/
getHttpUriForMxc: function(baseUrl, mxc, width, height,
resizeMethod, allowDirectLinks) {
if (typeof mxc !== "string" || !mxc) {
return '';
}
if (mxc.indexOf("mxc://") !== 0) {
if (allowDirectLinks) {
return mxc;
} else {
return '';
}
}
let serverAndMediaId = mxc.slice(6); // strips mxc://
let prefix = "/_matrix/media/v1/download/";
const params = {};
if (width) {
params.width = width;
}
if (height) {
params.height = height;
}
if (resizeMethod) {
params.method = resizeMethod;
}
if (utils.keys(params).length > 0) {
// these are thumbnailing params so they probably want the
// thumbnailing API...
prefix = "/_matrix/media/v1/thumbnail/";
}
const fragmentOffset = serverAndMediaId.indexOf("#");
let fragment = "";
if (fragmentOffset >= 0) {
fragment = serverAndMediaId.substr(fragmentOffset);
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
}
return baseUrl + prefix + serverAndMediaId +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params))) + fragment;
},
/**
* Get an identicon URL from an arbitrary string.
* @param {string} baseUrl The base homeserver url which has a content repo.
* @param {string} identiconString The string to create an identicon for.
* @param {Number} width The desired width of the image in pixels. Default: 96.
* @param {Number} height The desired height of the image in pixels. Default: 96.
* @return {string} The complete URL to the identicon.
*/
getIdenticonUri: function(baseUrl, identiconString, width, height) {
if (!identiconString) {
return null;
}
if (!width) {
width = 96;
}
if (!height) {
height = 96;
}
const params = {
width: width,
height: height,
};
const path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
$ident: identiconString,
});
return baseUrl + path +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params)));
},
};
+440
View File
@@ -0,0 +1,440 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module crypto/DeviceList
*
* Manages the list of other users' devices
*/
import q from 'q';
import DeviceInfo from './deviceinfo';
import olmlib from './olmlib';
/**
* @alias module:crypto/DeviceList
*/
export default class DeviceList {
constructor(baseApis, sessionStore, olmDevice) {
this._baseApis = baseApis;
this._sessionStore = sessionStore;
this._olmDevice = olmDevice;
// users with outdated device lists
// userId -> true
this._pendingUsersWithNewDevices = {};
// userId -> promise
this._keyDownloadsInProgressByUser = {};
this.lastKnownSyncToken = null;
}
/**
* Download the keys for a list of users and stores the keys in the session
* store.
* @param {Array} userIds The users to fetch.
* @param {bool} forceDownload Always download the keys even if cached.
*
* @return {Promise} A promise which resolves to a map userId->deviceId->{@link
* module:crypto/deviceinfo|DeviceInfo}.
*/
downloadKeys(userIds, forceDownload) {
// promises we need to wait for while the download happens
const promises = [];
let needsRefresh = false;
userIds.forEach((u) => {
if (this._keyDownloadsInProgressByUser[u]) {
// just wait for the existing download to complete
promises.push(this._keyDownloadsInProgressByUser[u]);
} else {
if (forceDownload || !this.getStoredDevicesForUser(u)) {
this.invalidateUserDeviceList(u);
}
if (this._pendingUsersWithNewDevices[u]) {
needsRefresh = true;
}
}
});
if (needsRefresh) {
promises.push(this.refreshOutdatedDeviceLists(true));
}
return q.all(promises).then(() => {
return this._getDevicesFromStore(userIds);
});
}
/**
* Get the stored device keys for a list of user ids
*
* @param {string[]} userIds the list of users to list keys for.
*
* @return {Object} userId->deviceId->{@link module:crypto/deviceinfo|DeviceInfo}.
*/
_getDevicesFromStore(userIds) {
const stored = {};
const self = this;
userIds.map(function(u) {
stored[u] = {};
const devices = self.getStoredDevicesForUser(u) || [];
devices.map(function(dev) {
stored[u][dev.deviceId] = dev;
});
});
return stored;
}
/**
* Get the stored device keys for a user id
*
* @param {string} userId the user to list keys for.
*
* @return {module:crypto/deviceinfo[]|null} list of devices, or null if we haven't
* managed to get a list of devices for this user yet.
*/
getStoredDevicesForUser(userId) {
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
if (!devs) {
return null;
}
const res = [];
for (const deviceId in devs) {
if (devs.hasOwnProperty(deviceId)) {
res.push(DeviceInfo.fromStorage(devs[deviceId], deviceId));
}
}
return res;
}
/**
* Get the stored keys for a single device
*
* @param {string} userId
* @param {string} deviceId
*
* @return {module:crypto/deviceinfo?} device, or undefined
* if we don't know about this device
*/
getStoredDevice(userId, deviceId) {
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
if (!devs || !devs[deviceId]) {
return undefined;
}
return DeviceInfo.fromStorage(devs[deviceId], deviceId);
}
/**
* Find a device by curve25519 identity key
*
* @param {string} userId owner of the device
* @param {string} algorithm encryption algorithm
* @param {string} 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 refreshOutdatedDeviceLists() for that.
*
* @param {String} userId
*/
invalidateUserDeviceList(userId) {
// sanity-check the userId. This is mostly paranoia, but if synapse
// can't parse the userId we give it as an mxid, it 500s the whole
// request and we can never update the device lists again (because
// the broken userId is always 'invalid' and always included in any
// refresh request).
// By checking it is at least a string, we can eliminate a class of
// silly errors.
if (typeof userId !== 'string') {
throw new Error('userId must be a string; was '+userId);
}
this._pendingUsersWithNewDevices[userId] = true;
}
/**
* Start device queries for any users with outdated device lists
*
* We tolerate multiple concurrent device queries, but only one query per
* user.
*
* If any users already have downloads in progress, they are ignored - they
* will be refreshed when the current download completes anyway, so
* each user with outdated device lists will be updated eventually.
*
* The returned promise resolves immediately if there are no users with
* outdated device lists, or if all users with outdated device lists already
* have a query in progress.
*
* Otherwise, a new query request is made, and the promise resolves
* once that query completes. If the query fails, the promise will reject
* if rejectOnFailure was truthy, otherwise it will still resolve.
*
* @param {Boolean?} rejectOnFailure true to make the returned promise
* reject if the device list query fails.
*
* @return {Promise}
*/
refreshOutdatedDeviceLists(rejectOnFailure) {
const users = Object.keys(this._pendingUsersWithNewDevices).filter(
(u) => !this._keyDownloadsInProgressByUser[u],
);
if (users.length === 0) {
return q();
}
let prom = this._doKeyDownloadForUsers(users).then(() => {
users.forEach((u) => {
delete this._keyDownloadsInProgressByUser[u];
});
// flush out any more requests that were blocked up while that
// was going on, but let the initial promise complete now.
//
this.refreshOutdatedDeviceLists().done();
}, (e) => {
console.error(
'Error updating device key cache for ' + users + ":", e,
);
// reinstate the pending flags on any users which failed; this will
// mean that we will do another download in the future, but won't
// tight-loop.
//
users.forEach((u) => {
delete this._keyDownloadsInProgressByUser[u];
this._pendingUsersWithNewDevices[u] = true;
});
// TODO: schedule a retry.
throw e;
});
users.forEach((u) => {
delete this._pendingUsersWithNewDevices[u];
this._keyDownloadsInProgressByUser[u] = prom;
});
if (!rejectOnFailure) {
// normally we just want to swallow the exception - we've already
// logged it futher up.
prom = prom.catch((e) => {});
}
return prom;
}
/**
* @param {string[]} downloadUsers list of userIds
*
* @return {Promise}
*/
_doKeyDownloadForUsers(downloadUsers) {
console.log('Starting key download for ' + downloadUsers);
const token = this.lastKnownSyncToken;
const opts = {};
if (token) {
opts.token = token;
}
return this._baseApis.downloadKeysForUsers(
downloadUsers, opts,
).then((res) => {
const dk = res.device_keys || {};
for (const userId of downloadUsers) {
console.log('got keys for ' + userId + ':', dk[userId]);
// map from deviceid -> deviceinfo for this user
const userStore = {};
const devs = this._sessionStore.getEndToEndDevicesForUser(userId);
if (devs) {
Object.keys(devs).forEach((deviceId) => {
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
userStore[deviceId] = d;
});
}
_updateStoredDeviceKeysForUser(
this._olmDevice, userId, userStore, dk[userId] || {},
);
// update the session store
const storage = {};
Object.keys(userStore).forEach((deviceId) => {
storage[deviceId] = userStore[deviceId].toStorage();
});
this._sessionStore.storeEndToEndDevicesForUser(
userId, storage,
);
if (token) {
this._sessionStore.storeEndToEndDeviceSyncToken(token);
}
}
});
}
}
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;
}
+860
View File
@@ -0,0 +1,860 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* olm.js wrapper
*
* @module crypto/OlmDevice
*/
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
*/
/**
* Manages the olm cryptography functions. Each OlmDevice has a single
* OlmAccount and a number of OlmSessions.
*
* Accounts and sessions are kept pickled in a sessionStore.
*
* @constructor
* @alias module:crypto/OlmDevice
*
* @param {Object} sessionStore A store to be used for data in end-to-end
* crypto
*
* @property {string} deviceCurve25519Key Curve25519 key for the account
* @property {string} deviceEd25519Key Ed25519 key for the account
*/
function OlmDevice(sessionStore) {
this._sessionStore = sessionStore;
this._pickleKey = "DEFAULT_KEY";
let e2eKeys;
const account = new Olm.Account();
try {
_initialise_account(this._sessionStore, this._pickleKey, account);
e2eKeys = JSON.parse(account.identity_keys());
} finally {
account.free();
}
this.deviceCurve25519Key = e2eKeys.curve25519;
this.deviceEd25519Key = e2eKeys.ed25519;
// we don't bother stashing outboundgroupsessions in the sessionstore -
// instead we keep them here.
this._outboundGroupSessionStore = {};
// Store a set of decrypted message indexes for each group session.
// This partially mitigates a replay attack where a MITM resends a group
// message into the room.
//
// TODO: If we ever remove an event from memory we will also need to remove
// it from this map. Otherwise if we download the event from the server we
// will think that it is a duplicate.
//
// Keys are strings of form "<senderKey>|<session_id>|<message_index>"
// Values are true.
this._inboundGroupSessionMessageIndexes = {};
}
function _initialise_account(sessionStore, pickleKey, account) {
const e2eAccount = sessionStore.getEndToEndAccount();
if (e2eAccount !== null) {
account.unpickle(pickleKey, e2eAccount);
return;
}
account.create();
const pickled = account.pickle(pickleKey);
sessionStore.storeEndToEndAccount(pickled);
}
/**
* @return {array} The version of Olm.
*/
OlmDevice.getOlmVersion = function() {
return Olm.get_library_version();
};
/**
* extract our OlmAccount from the session store and call the given function
*
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getAccount = function(func) {
const account = new Olm.Account();
try {
const pickledAccount = this._sessionStore.getEndToEndAccount();
account.unpickle(this._pickleKey, pickledAccount);
return func(account);
} finally {
account.free();
}
};
/**
* store our OlmAccount in the session store
*
* @param {OlmAccount} account
* @private
*/
OlmDevice.prototype._saveAccount = function(account) {
const pickledAccount = account.pickle(this._pickleKey);
this._sessionStore.storeEndToEndAccount(pickledAccount);
};
/**
* extract an OlmSession from the session store and call the given function
*
* @param {string} deviceKey
* @param {string} sessionId
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getSession = function(deviceKey, sessionId, func) {
const sessions = this._sessionStore.getEndToEndSessions(deviceKey);
const pickledSession = sessions[sessionId];
const session = new Olm.Session();
try {
session.unpickle(this._pickleKey, pickledSession);
return func(session);
} finally {
session.free();
}
};
/**
* store our OlmSession in the session store
*
* @param {string} deviceKey
* @param {OlmSession} session
* @private
*/
OlmDevice.prototype._saveSession = function(deviceKey, session) {
const pickledSession = session.pickle(this._pickleKey);
this._sessionStore.storeEndToEndSession(
deviceKey, session.session_id(), pickledSession,
);
};
/**
* get an OlmUtility and call the given function
*
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getUtility = function(func) {
const utility = new Olm.Utility();
try {
return func(utility);
} finally {
utility.free();
}
};
/**
* Signs a message with the ed25519 key for this account.
*
* @param {string} message message to be signed
* @return {string} base64-encoded signature
*/
OlmDevice.prototype.sign = function(message) {
return this._getAccount(function(account) {
return account.sign(message);
});
};
/**
* Get the current (unused, unpublished) one-time keys for this account.
*
* @return {object} one time keys; an object with the single property
* <tt>curve25519</tt>, which is itself an object mapping key id to Curve25519
* key.
*/
OlmDevice.prototype.getOneTimeKeys = function() {
return this._getAccount(function(account) {
return JSON.parse(account.one_time_keys());
});
};
/**
* Get the maximum number of one-time keys we can store.
*
* @return {number} number of keys
*/
OlmDevice.prototype.maxNumberOfOneTimeKeys = function() {
return this._getAccount(function(account) {
return account.max_number_of_one_time_keys();
});
};
/**
* Marks all of the one-time keys as published.
*/
OlmDevice.prototype.markKeysAsPublished = function() {
const self = this;
this._getAccount(function(account) {
account.mark_keys_as_published();
self._saveAccount(account);
});
};
/**
* Generate some new one-time keys
*
* @param {number} numKeys number of keys to generate
*/
OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
const self = this;
this._getAccount(function(account) {
account.generate_one_time_keys(numKeys);
self._saveAccount(account);
});
};
/**
* Generate a new outbound session
*
* The new session will be stored in the sessionStore.
*
* @param {string} theirIdentityKey remote user's Curve25519 identity key
* @param {string} theirOneTimeKey remote user's one-time Curve25519 key
* @return {string} sessionId for the outbound session.
*/
OlmDevice.prototype.createOutboundSession = function(
theirIdentityKey, theirOneTimeKey,
) {
const self = this;
return this._getAccount(function(account) {
const session = new Olm.Session();
try {
session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
self._saveSession(theirIdentityKey, session);
return session.session_id();
} finally {
session.free();
}
});
};
/**
* Generate a new inbound session, given an incoming message
*
* @param {string} theirDeviceIdentityKey remote user's Curve25519 identity key
* @param {number} message_type message_type field from the received message (must be 0)
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {{payload: string, session_id: string}} decrypted payload, and
* session id of new session
*
* @raises {Error} if the received message was not valid (for instance, it
* didn't use a valid one-time key).
*/
OlmDevice.prototype.createInboundSession = function(
theirDeviceIdentityKey, message_type, ciphertext,
) {
if (message_type !== 0) {
throw new Error("Need message_type == 0 to create inbound session");
}
const self = this;
return this._getAccount(function(account) {
const session = new Olm.Session();
try {
session.create_inbound_from(account, theirDeviceIdentityKey, ciphertext);
account.remove_one_time_keys(session);
self._saveAccount(account);
const payloadString = session.decrypt(message_type, ciphertext);
self._saveSession(theirDeviceIdentityKey, session);
return {
payload: payloadString,
session_id: session.session_id(),
};
} finally {
session.free();
}
});
};
/**
* Get a list of known session IDs for the given device
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @return {string[]} a list of known session ids for the device
*/
OlmDevice.prototype.getSessionIdsForDevice = function(theirDeviceIdentityKey) {
const sessions = this._sessionStore.getEndToEndSessions(
theirDeviceIdentityKey,
);
return utils.keys(sessions);
};
/**
* Get the right olm session id for encrypting messages to the given identity key
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @return {string?} session id, or null if no established session
*/
OlmDevice.prototype.getSessionIdForDevice = function(theirDeviceIdentityKey) {
const sessionIds = this.getSessionIdsForDevice(theirDeviceIdentityKey);
if (sessionIds.length === 0) {
return null;
}
// Use the session with the lowest ID.
sessionIds.sort();
return sessionIds[0];
};
/**
* Get information on the active Olm sessions for a device.
* <p>
* Returns an array, with an entry for each active session. The first entry in
* the result will be the one used for outgoing messages. Each entry contains
* the keys 'hasReceivedMessage' (true if the session has received an incoming
* message and is therefore past the pre-key stage), and 'sessionId'.
*
* @param {string} deviceIdentityKey Curve25519 identity key for the device
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
*/
OlmDevice.prototype.getSessionInfoForDevice = function(deviceIdentityKey) {
const sessionIds = this.getSessionIdsForDevice(deviceIdentityKey);
sessionIds.sort();
const info = [];
function getSessionInfo(session) {
return {
hasReceivedMessage: session.has_received_message(),
};
}
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);
}
return info;
};
/**
* Encrypt an outgoing message using an existing session
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {string} payloadString payload to be encrypted and sent
*
* @return {string} ciphertext
*/
OlmDevice.prototype.encryptMessage = function(
theirDeviceIdentityKey, sessionId, payloadString,
) {
const self = this;
checkPayloadLength(payloadString);
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
const res = session.encrypt(payloadString);
self._saveSession(theirDeviceIdentityKey, session);
return res;
});
};
/**
* Decrypt an incoming message using an existing session
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {number} message_type message_type field from the received message
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {string} decrypted payload.
*/
OlmDevice.prototype.decryptMessage = function(
theirDeviceIdentityKey, sessionId, message_type, ciphertext,
) {
const self = this;
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
const payloadString = session.decrypt(message_type, ciphertext);
self._saveSession(theirDeviceIdentityKey, session);
return payloadString;
});
};
/**
* Determine if an incoming messages is a prekey message matching an existing session
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {number} message_type message_type field from the received message
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {boolean} true if the received message is a prekey message which matches
* the given session.
*/
OlmDevice.prototype.matchesSession = function(
theirDeviceIdentityKey, sessionId, message_type, ciphertext,
) {
if (message_type !== 0) {
return false;
}
return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
return session.matches_inbound(ciphertext);
});
};
// Outbound group session
// ======================
/**
* store an OutboundGroupSession in _outboundGroupSessionStore
*
* @param {Olm.OutboundGroupSession} session
* @private
*/
OlmDevice.prototype._saveOutboundGroupSession = function(session) {
const pickledSession = session.pickle(this._pickleKey);
this._outboundGroupSessionStore[session.session_id()] = pickledSession;
};
/**
* extract an OutboundGroupSession from _outboundGroupSessionStore and call the
* given function
*
* @param {string} sessionId
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
const pickled = this._outboundGroupSessionStore[sessionId];
if (pickled === null) {
throw new Error("Unknown outbound group session " + sessionId);
}
const session = new Olm.OutboundGroupSession();
try {
session.unpickle(this._pickleKey, pickled);
return func(session);
} finally {
session.free();
}
};
/**
* Generate a new outbound group session
*
* @return {string} sessionId for the outbound session.
*/
OlmDevice.prototype.createOutboundGroupSession = function() {
const session = new Olm.OutboundGroupSession();
try {
session.create();
this._saveOutboundGroupSession(session);
return session.session_id();
} finally {
session.free();
}
};
/**
* Encrypt an outgoing message with an outbound group session
*
* @param {string} sessionId the id of the outboundgroupsession
* @param {string} payloadString payload to be encrypted and sent
*
* @return {string} ciphertext
*/
OlmDevice.prototype.encryptGroupMessage = function(sessionId, payloadString) {
const self = this;
checkPayloadLength(payloadString);
return this._getOutboundGroupSession(sessionId, function(session) {
const res = session.encrypt(payloadString);
self._saveOutboundGroupSession(session);
return res;
});
};
/**
* Get the session keys for an outbound group session
*
* @param {string} sessionId the id of the outbound group session
*
* @return {{chain_index: number, key: string}} current chain index, and
* base64-encoded secret key.
*/
OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
return this._getOutboundGroupSession(sessionId, function(session) {
return {
chain_index: session.message_index(),
key: session.session_key(),
};
});
};
// Inbound group session
// =====================
/**
* store an InboundGroupSession in the session store
*
* @param {string} roomId
* @param {string} senderCurve25519Key
* @param {string} sessionId
* @param {Olm.InboundGroupSession} session
* @param {object} keysClaimed Other keys the sender claims.
* @private
*/
OlmDevice.prototype._saveInboundGroupSession = function(
roomId, senderCurve25519Key, sessionId, session, keysClaimed,
) {
const r = {
room_id: roomId,
session: session.pickle(this._pickleKey),
keysClaimed: keysClaimed,
};
this._sessionStore.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, JSON.stringify(r),
);
};
/**
* extract an InboundGroupSession from the session store and call the given function
*
* @param {string} roomId
* @param {string} senderKey
* @param {string} sessionId
* @param {function(Olm.InboundGroupSession, Object<string, string>): T} func
* function to call. Second argument is the map of keys claimed by the session.
*
* @return {null} the sessionId is unknown
*
* @return {T} result of func
*
* @private
* @template {T}
*/
OlmDevice.prototype._getInboundGroupSession = function(
roomId, senderKey, sessionId, func,
) {
let r = this._sessionStore.getEndToEndInboundGroupSession(
senderKey, sessionId,
);
if (r === null) {
return null;
}
r = JSON.parse(r);
// check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room.
if (roomId !== r.room_id) {
throw new Error(
"Mismatched room_id for inbound group session (expected " + r.room_id +
", was " + roomId + ")",
);
}
const session = new Olm.InboundGroupSession();
try {
session.unpickle(this._pickleKey, r.session);
return func(session, r.keysClaimed || {});
} finally {
session.free();
}
};
/**
* Add an inbound group session to the session store
*
* @param {string} roomId room in which this session will be used
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {string} sessionKey base64-encoded secret key
* @param {Object<string, string>} keysClaimed Other keys the sender claims.
*/
OlmDevice.prototype.addInboundGroupSession = function(
roomId, senderKey, sessionId, sessionKey, keysClaimed,
) {
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,
);
}
self._saveInboundGroupSession(
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();
}
};
/**
* Decrypt a received message with an inbound group session
*
* @param {string} roomId room in which the message was received
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {string} body base64-encoded body of the encrypted message
*
* @return {null} the sessionId is unknown
*
* @return {{result: string, keysProved: Object<string, string>, keysClaimed:
* Object<string, string>}} result
*/
OlmDevice.prototype.decryptGroupMessage = function(
roomId, senderKey, sessionId, body,
) {
const self = this;
function decrypt(session, keysClaimed) {
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.
const keysProved = {curve25519: senderKey};
self._saveInboundGroupSession(
roomId, senderKey, sessionId, session, keysClaimed,
);
return {
result: plaintext,
keysClaimed: keysClaimed,
keysProved: keysProved,
};
}
return this._getInboundGroupSession(
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
// =========
/**
* Verify an ed25519 signature.
*
* @param {string} key ed25519 key
* @param {string} message message which was signed
* @param {string} signature base64-encoded signature to be checked
*
* @raises {Error} if there is a problem with the verification. If the key was
* too small then the message will be "OLM.INVALID_BASE64". If the signature
* was invalid then the message will be "OLM.BAD_MESSAGE_MAC".
*/
OlmDevice.prototype.verifySignature = function(
key, message, signature,
) {
this._getUtility(function(util) {
util.ed25519_verify(key, message, signature);
});
};
/** */
module.exports = OlmDevice;
+193
View File
@@ -0,0 +1,193 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* Internal module. Defines the base classes of the encryption implementations
*
* @module crypto/algorithms/base
*/
const utils = require("../../utils");
/**
* map of registered encryption algorithm classes. A map from string to {@link
* module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm} class
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.EncryptionAlgorithm)>}
*/
module.exports.ENCRYPTION_CLASSES = {};
/**
* map of registered encryption algorithm classes. Map from string to {@link
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm} class
*
* @type {Object.<string, function(new: module:crypto/algorithms/base.DecryptionAlgorithm)>}
*/
module.exports.DECRYPTION_CLASSES = {};
/**
* base type for encryption implementations
*
* @constructor
* @alias module:crypto/algorithms/base.EncryptionAlgorithm
*
* @param {object} params parameters
* @param {string} params.userId The UserID for the local user
* @param {string} params.deviceId The identifier for this device.
* @param {module:crypto} params.crypto crypto core
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
* @param {string} params.roomId The ID of the room we will be sending to
* @param {object} params.config The body of the m.room.encryption event
*/
const EncryptionAlgorithm = function(params) {
this._userId = params.userId;
this._deviceId = params.deviceId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._baseApis = params.baseApis;
this._roomId = params.roomId;
};
/** */
module.exports.EncryptionAlgorithm = EncryptionAlgorithm;
/**
* Encrypt a message event
*
* @method module:crypto/algorithms/base.EncryptionAlgorithm#encryptMessage
* @abstract
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
/**
* Called when the membership of a member of the room changes.
*
* @param {module:models/event.MatrixEvent} event event causing the change
* @param {module:models/room-member} member user whose membership changed
* @param {string=} oldMembership previous membership
*/
EncryptionAlgorithm.prototype.onRoomMembership = function(
event, member, oldMembership,
) {};
/**
* base type for decryption implementations
*
* @constructor
* @alias module:crypto/algorithms/base.DecryptionAlgorithm
*
* @param {object} params parameters
* @param {string} params.userId The UserID for the local user
* @param {module:crypto} params.crypto crypto core
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
* @param {string=} params.roomId The ID of the room we will be receiving
* from. Null for to-device events.
*/
const DecryptionAlgorithm = function(params) {
this._userId = params.userId;
this._crypto = params.crypto;
this._olmDevice = params.olmDevice;
this._roomId = params.roomId;
};
/** */
module.exports.DecryptionAlgorithm = DecryptionAlgorithm;
/**
* Decrypt an event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#decryptEvent
* @abstract
*
* @param {object} event raw event
*
* @return {null} if the event referred to an unknown megolm session
* @return {module:crypto.DecryptionResult} decryption result
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
/**
* Handle a key event
*
* @method module:crypto/algorithms/base.DecryptionAlgorithm#onRoomKeyEvent
*
* @param {module:models/event.MatrixEvent} 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
*
* @constructor
* @param {string} msg message describing the problem
* @extends Error
*/
module.exports.DecryptionError = function(msg) {
this.message = 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
*
* @param {string} algorithm algorithm tag to register for
*
* @param {class} encryptor {@link
* module:crypto/algorithms/base.EncryptionAlgorithm|EncryptionAlgorithm}
* implementation
*
* @param {class} decryptor {@link
* module:crypto/algorithms/base.DecryptionAlgorithm|DecryptionAlgorithm}
* implementation
*/
module.exports.registerAlgorithm = function(algorithm, encryptor, decryptor) {
module.exports.ENCRYPTION_CLASSES[algorithm] = encryptor;
module.exports.DECRYPTION_CLASSES[algorithm] = decryptor;
};
+40
View File
@@ -0,0 +1,40 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module crypto/algorithms
*/
const base = require("./base");
require("./olm");
require("./megolm");
/**
* @see module:crypto/algorithms/base.ENCRYPTION_CLASSES
*/
module.exports.ENCRYPTION_CLASSES = base.ENCRYPTION_CLASSES;
/**
* @see module:crypto/algorithms/base.DECRYPTION_CLASSES
*/
module.exports.DECRYPTION_CLASSES = base.DECRYPTION_CLASSES;
/**
* @see module:crypto/algorithms/base.DecryptionError
*/
module.exports.DecryptionError = base.DecryptionError;
+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,
);
+319
View File
@@ -0,0 +1,319 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* Defines m.olm encryption/decryption
*
* @module crypto/algorithms/olm
*/
const q = require('q');
const utils = require("../../utils");
const olmlib = require("../olmlib");
const DeviceInfo = require("../deviceinfo");
const DeviceVerification = DeviceInfo.DeviceVerification;
const base = require("./base");
/**
* Olm encryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.EncryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.EncryptionAlgorithm}
*/
function OlmEncryption(params) {
base.EncryptionAlgorithm.call(this, params);
this._sessionPrepared = false;
this._prepPromise = null;
}
utils.inherits(OlmEncryption, base.EncryptionAlgorithm);
/**
* @private
* @param {string[]} roomMembers list of currently-joined users in the room
* @return {module:client.Promise} Promise which resolves when setup is complete
*/
OlmEncryption.prototype._ensureSession = function(roomMembers) {
if (this._prepPromise) {
// prep already in progress
return this._prepPromise;
}
if (this._sessionPrepared) {
// prep already done
return q();
}
const self = this;
this._prepPromise = self._crypto.downloadKeys(roomMembers, true).then(function(res) {
return self._crypto.ensureOlmSessionsForUsers(roomMembers);
}).then(function() {
self._sessionPrepared = true;
}).finally(function() {
self._prepPromise = null;
});
return this._prepPromise;
};
/**
* @inheritdoc
*
* @param {module:models/room} room
* @param {string} eventType
* @param {object} content plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
// pick the list of recipients based on the membership list.
//
// TODO: there is a race condition here! What if a new user turns up
// just as you are sending a secret message?
const users = utils.map(room.getJoinedMembers(), function(u) {
return u.userId;
});
const self = this;
return this._ensureSession(users).then(function() {
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 sending to ourself
continue;
}
if (deviceInfo.verified == DeviceVerification.BLOCKED) {
// don't bother setting up sessions with blocked users
continue;
}
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
self._userId, self._deviceId, self._olmDevice,
userId, deviceInfo, payloadFields,
);
}
}
return encryptedContent;
});
};
/**
* Olm decryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.DecryptionAlgorithm}
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.DecryptionAlgorithm}
*/
function OlmDecryption(params) {
base.DecryptionAlgorithm.call(this, params);
}
utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
/**
* @inheritdoc
*
* @param {MatrixEvent} event
*
* @throws {module:crypto/algorithms/base.DecryptionError} if there is a
* problem decrypting the event
*/
OlmDecryption.prototype.decryptEvent = function(event) {
const content = event.getWireContent();
const deviceKey = content.sender_key;
const ciphertext = content.ciphertext;
if (!ciphertext) {
throw new base.DecryptionError("Missing ciphertext");
}
if (!(this._olmDevice.deviceCurve25519Key in ciphertext)) {
throw new base.DecryptionError("Not included in recipients");
}
const message = ciphertext[this._olmDevice.deviceCurve25519Key];
let payloadString;
try {
payloadString = this._decryptMessage(deviceKey, message);
} catch (e) {
console.warn(
"Failed to decrypt Olm event (id=" +
event.getId() + ") from " + deviceKey +
": " + e.message,
);
throw new base.DecryptionError("Bad Encrypted Message");
}
const payload = JSON.parse(payloadString);
// check that we were the intended recipient, to avoid unknown-key attack
// https://github.com/vector-im/vector-web/issues/2483
if (payload.recipient != this._userId) {
console.warn(
"Event " + event.getId() + ": Intended recipient " +
payload.recipient + " does not match our id " + this._userId,
);
throw new base.DecryptionError(
"Message was intented for " + payload.recipient,
);
}
if (payload.recipient_keys.ed25519 !=
this._olmDevice.deviceEd25519Key) {
console.warn(
"Event " + event.getId() + ": Intended recipient ed25519 key " +
payload.recipient_keys.ed25519 + " did not match ours",
);
throw new base.DecryptionError("Message not intended for this device");
}
// check that the original sender matches what the homeserver told us, to
// avoid people masquerading as others.
// (this check is also provided via the sender's embedded ed25519 key,
// which is checked elsewhere).
if (payload.sender != event.getSender()) {
console.warn(
"Event " + event.getId() + ": original sender " + payload.sender +
" does not match reported sender " + event.getSender(),
);
throw new base.DecryptionError(
"Message forwarded from " + payload.sender,
);
}
// Olm events intended for a room have a room_id.
if (payload.room_id !== event.getRoomId()) {
console.warn(
"Event " + event.getId() + ": original room " + payload.room_id +
" does not match reported room " + event.room_id,
);
throw new base.DecryptionError(
"Message intended for room " + payload.room_id,
);
}
event.setClearData(payload, {curve25519: deviceKey}, payload.keys || {});
};
/**
* Attempt to decrypt an Olm message
*
* @param {string} theirDeviceIdentityKey Curve25519 identity key of the sender
* @param {object} message message object, with 'type' and 'body' fields
*
* @return {string} payload, if decrypted successfully.
*/
OlmDecryption.prototype._decryptMessage = function(theirDeviceIdentityKey, message) {
const sessionIds = this._olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
// try each session in turn.
const decryptionErrors = {};
for (let i = 0; i < sessionIds.length; i++) {
const sessionId = sessionIds[i];
try {
const payload = this._olmDevice.decryptMessage(
theirDeviceIdentityKey, sessionId, message.type, message.body,
);
console.log(
"Decrypted Olm message from " + theirDeviceIdentityKey +
" with session " + sessionId,
);
return payload;
} catch (e) {
const foundSession = this._olmDevice.matchesSession(
theirDeviceIdentityKey, sessionId, message.type, message.body,
);
if (foundSession) {
// decryption failed, but it was a prekey message matching this
// session, so it should have worked.
throw new Error(
"Error decrypting prekey message with existing session id " +
sessionId + ": " + e.message,
);
}
// otherwise it's probably a message for another session; carry on, but
// keep a record of the error
decryptionErrors[sessionId] = e.message;
}
}
if (message.type !== 0) {
// not a prekey message, so it should have matched an existing session, but it
// didn't work.
if (sessionIds.length === 0) {
throw new Error("No existing sessions");
}
throw new Error(
"Error decrypting non-prekey message with existing sessions: " +
JSON.stringify(decryptionErrors),
);
}
// prekey message which doesn't match any existing sessions: make a new
// session.
let res;
try {
res = this._olmDevice.createInboundSession(
theirDeviceIdentityKey, message.type, message.body,
);
} catch (e) {
decryptionErrors["(new)"] = e.message;
throw new Error(
"Error decrypting prekey message: " +
JSON.stringify(decryptionErrors),
);
}
console.log(
"created new inbound Olm session ID " +
res.session_id + " with " + theirDeviceIdentityKey,
);
return res.payload;
};
base.registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
+169
View File
@@ -0,0 +1,169 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module crypto/deviceinfo
*/
/**
* Information about a user's device
*
* @constructor
* @alias module:crypto/deviceinfo
*
* @property {string} deviceId the ID of this device
*
* @property {string[]} algorithms list of algorithms supported by this device
*
* @property {Object.<string,string>} keys a map from
* &lt;key type&gt;:&lt;id&gt; -> &lt;base64-encoded key&gt;>
*
* @property {module:crypto/deviceinfo.DeviceVerification} verified
* whether the device has been verified/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
*
* @param {string} deviceId id of the device
*/
function DeviceInfo(deviceId) {
// you can't change the deviceId
Object.defineProperty(this, 'deviceId', {
enumerable: true,
value: deviceId,
});
this.algorithms = [];
this.keys = {};
this.verified = DeviceVerification.UNVERIFIED;
this.known = false;
this.unsigned = {};
}
/**
* rehydrate a DeviceInfo from the session store
*
* @param {object} obj raw object from session store
* @param {string} deviceId id of the device
*
* @return {module:crypto~DeviceInfo} new DeviceInfo
*/
DeviceInfo.fromStorage = function(obj, deviceId) {
const res = new DeviceInfo(deviceId);
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
res[prop] = obj[prop];
}
}
return res;
};
/**
* Prepare a DeviceInfo for JSON serialisation in the session store
*
* @return {object} deviceinfo with non-serialised members removed
*/
DeviceInfo.prototype.toStorage = function() {
return {
algorithms: this.algorithms,
keys: this.keys,
verified: this.verified,
known: this.known,
unsigned: this.unsigned,
};
};
/**
* Get the fingerprint for this device (ie, the Ed25519 key)
*
* @return {string} base64-encoded fingerprint of this device
*/
DeviceInfo.prototype.getFingerprint = function() {
return this.keys["ed25519:" + this.deviceId];
};
/**
* Get the identity key for this device (ie, the Curve25519 key)
*
* @return {string} base64-encoded identity key of this device
*/
DeviceInfo.prototype.getIdentityKey = function() {
return this.keys["curve25519:" + this.deviceId];
};
/**
* Get the configured display name for this device, if any
*
* @return {string?} displayname
*/
DeviceInfo.prototype.getDisplayName = function() {
return this.unsigned.device_display_name || null;
};
/**
* Returns true if this device is blocked
*
* @return {Boolean} true if blocked
*/
DeviceInfo.prototype.isBlocked = function() {
return this.verified == DeviceVerification.BLOCKED;
};
/**
* Returns true if this device is verified
*
* @return {Boolean} true if verified
*/
DeviceInfo.prototype.isVerified = function() {
return this.verified == DeviceVerification.VERIFIED;
};
/**
* 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
*/
DeviceInfo.DeviceVerification = {
VERIFIED: 1,
UNVERIFIED: 0,
BLOCKED: -1,
};
const DeviceVerification = DeviceInfo.DeviceVerification;
/** */
module.exports = DeviceInfo;
+1064
View File
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,
);
};
+145
View File
@@ -0,0 +1,145 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module filter-component
*/
/**
* Checks if a value matches a given field value, which may be a * terminated
* wildcard pattern.
* @param {String} actual_value The value to be compared
* @param {String} filter_value The filter pattern to be compared
* @return {bool} true if the actual_value matches the filter_value
*/
function _matches_wildcard(actual_value, filter_value) {
if (filter_value.endsWith("*")) {
const type_prefix = filter_value.slice(0, -1);
return actual_value.substr(0, type_prefix.length) === type_prefix;
} else {
return actual_value === filter_value;
}
}
/**
* FilterComponent is a section of a Filter definition which defines the
* types, rooms, senders filters etc to be applied to a particular type of resource.
* This is all ported over from synapse's Filter object.
*
* N.B. that synapse refers to these as 'Filters', and what js-sdk refers to as
* 'Filters' are referred to as 'FilterCollections'.
*
* @constructor
* @param {Object} filter_json the definition of this filter JSON, e.g. { 'contains_url': true }
*/
function FilterComponent(filter_json) {
this.filter_json = filter_json;
this.types = filter_json.types || null;
this.not_types = filter_json.not_types || [];
this.rooms = filter_json.rooms || null;
this.not_rooms = filter_json.not_rooms || [];
this.senders = filter_json.senders || null;
this.not_senders = filter_json.not_senders || [];
this.contains_url = filter_json.contains_url || null;
}
/**
* Checks with the filter component matches the given event
* @param {MatrixEvent} event event to be checked against the filter
* @return {bool} true if the event matches the filter
*/
FilterComponent.prototype.check = function(event) {
return this._checkFields(
event.getRoomId(),
event.getSender(),
event.getType(),
event.getContent() ? event.getContent().url !== undefined : false,
);
};
/**
* Checks whether the filter component matches the given event fields.
* @param {String} room_id the room_id for the event being checked
* @param {String} sender the sender of the event being checked
* @param {String} event_type the type of the event being checked
* @param {String} contains_url whether the event contains a content.url field
* @return {bool} true if the event fields match the filter
*/
FilterComponent.prototype._checkFields =
function(room_id, sender, event_type, contains_url) {
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);
},
};
const self = this;
Object.keys(literal_keys).forEach(function(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;
}
const allowed_values = self[name];
if (allowed_values) {
if (!allowed_values.map(match_func)) {
return false;
}
}
});
const contains_url_filter = this.filter_json.contains_url;
if (contains_url_filter !== undefined) {
if (contains_url_filter !== contains_url) {
return false;
}
}
return true;
};
/**
* Filters a list of events down to those which match this filter component
* @param {MatrixEvent[]} events Events to be checked againt the filter component
* @return {MatrixEvent[]} events which matched the filter component
*/
FilterComponent.prototype.filter = function(events) {
return events.filter(this.check, this);
};
/**
* Returns the limit field for a given filter component, providing a default of
* 10 if none is otherwise specified. Cargo-culted from Synapse.
* @return {Number} the limit for this filter component.
*/
FilterComponent.prototype.limit = function() {
return this.filter_json.limit !== undefined ? this.filter_json.limit : 10;
};
/** The FilterComponent class */
module.exports = FilterComponent;
+192
View File
@@ -0,0 +1,192 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module filter
*/
const FilterComponent = require("./filter-component");
/**
* @param {Object} obj
* @param {string} keyNesting
* @param {*} val
*/
function setProp(obj, keyNesting, val) {
const nestedKeys = keyNesting.split(".");
let currentObj = obj;
for (let i = 0; i < (nestedKeys.length - 1); i++) {
if (!currentObj[nestedKeys[i]]) {
currentObj[nestedKeys[i]] = {};
}
currentObj = currentObj[nestedKeys[i]];
}
currentObj[nestedKeys[nestedKeys.length - 1]] = val;
}
/**
* Construct a new Filter.
* @constructor
* @param {string} userId The user ID for this filter.
* @param {string=} filterId The filter ID if known.
* @prop {string} userId The user ID of the filter
* @prop {?string} filterId The filter ID
*/
function Filter(userId, filterId) {
this.userId = userId;
this.filterId = filterId;
this.definition = {};
}
/**
* Get the ID of this filter on your homeserver (if known)
* @return {?Number} The filter ID
*/
Filter.prototype.getFilterId = function() {
return this.filterId;
};
/**
* Get the JSON body of the filter.
* @return {Object} The filter definition
*/
Filter.prototype.getDefinition = function() {
return this.definition;
};
/**
* Set the JSON body of the filter
* @param {Object} definition The filter definition
*/
Filter.prototype.setDefinition = function(definition) {
this.definition = definition;
// This is all ported from synapse's FilterCollection()
// definitions look something like:
// {
// "room": {
// "rooms": ["!abcde:example.com"],
// "not_rooms": ["!123456:example.com"],
// "state": {
// "types": ["m.room.*"],
// "not_rooms": ["!726s6s6q:example.com"],
// },
// "timeline": {
// "limit": 10,
// "types": ["m.room.message"],
// "not_rooms": ["!726s6s6q:example.com"],
// "not_senders": ["@spam:example.com"]
// "contains_url": true
// },
// "ephemeral": {
// "types": ["m.receipt", "m.typing"],
// "not_rooms": ["!726s6s6q:example.com"],
// "not_senders": ["@spam:example.com"]
// }
// },
// "presence": {
// "types": ["m.presence"],
// "not_senders": ["@alice:example.com"]
// },
// "event_format": "client",
// "event_fields": ["type", "content", "sender"]
// }
const room_filter_json = definition.room;
// consider the top level rooms/not_rooms filter
const room_filter_fields = {};
if (room_filter_json) {
if (room_filter_json.rooms) {
room_filter_fields.rooms = room_filter_json.rooms;
}
if (room_filter_json.rooms) {
room_filter_fields.not_rooms = room_filter_json.not_rooms;
}
this._include_leave = room_filter_json.include_leave || false;
}
this._room_filter = new FilterComponent(room_filter_fields);
this._room_timeline_filter = new FilterComponent(
room_filter_json ? (room_filter_json.timeline || {}) : {},
);
// don't bother porting this from synapse yet:
// this._room_state_filter =
// new FilterComponent(room_filter_json.state || {});
// this._room_ephemeral_filter =
// new FilterComponent(room_filter_json.ephemeral || {});
// this._room_account_data_filter =
// new FilterComponent(room_filter_json.account_data || {});
// this._presence_filter =
// new FilterComponent(definition.presence || {});
// this._account_data_filter =
// new FilterComponent(definition.account_data || {});
};
/**
* Get the room.timeline filter component of the filter
* @return {FilterComponent} room timeline filter component
*/
Filter.prototype.getRoomTimelineFilterComponent = function() {
return this._room_timeline_filter;
};
/**
* Filter the list of events based on whether they are allowed in a timeline
* based on this filter
* @param {MatrixEvent[]} events the list of events being filtered
* @return {MatrixEvent[]} the list of events which match the filter
*/
Filter.prototype.filterRoomTimeline = function(events) {
return this._room_timeline_filter.filter(this._room_filter.filter(events));
};
/**
* Set the max number of events to return for each room's timeline.
* @param {Number} limit The max number of events to return for each room.
*/
Filter.prototype.setTimelineLimit = function(limit) {
setProp(this.definition, "room.timeline.limit", limit);
};
/**
* Control whether left rooms should be included in responses.
* @param {boolean} includeLeave True to make rooms the user has left appear
* in responses.
*/
Filter.prototype.setIncludeLeaveRooms = function(includeLeave) {
setProp(this.definition, "room.include_leave", includeLeave);
};
/**
* Create a filter from existing data.
* @static
* @param {string} userId
* @param {string} filterId
* @param {Object} jsonObj
* @return {Filter}
*/
Filter.fromJson = function(userId, filterId, jsonObj) {
const filter = new Filter(userId, filterId);
filter.setDefinition(jsonObj);
return filter;
};
/** The Filter class */
module.exports = Filter;
+769
View File
@@ -0,0 +1,769 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module. See {@link MatrixHttpApi} for the public class.
* @module http-api
*/
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.
const callbacks = require("./realtime-callbacks");
/*
TODO:
- CS: complete register function (doing stages)
- Identity server: linkEmail, authEmail, bindEmail, lookup3pid
*/
/**
* A constant representing the URI path for release 0 of the Client-Server HTTP API.
*/
module.exports.PREFIX_R0 = "/_matrix/client/r0";
/**
* A constant representing the URI path for as-yet unspecified Client-Server HTTP APIs.
*/
module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable";
/**
* URI path for the identity API
*/
module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
/**
* URI path for the media repo API
*/
module.exports.PREFIX_MEDIA_R0 = "/_matrix/media/r0";
/**
* Construct a MatrixHttpApi.
* @constructor
* @param {EventEmitter} event_emitter The event emitter to use for emitting events
* @param {Object} opts The options to use for this HTTP API.
* @param {string} opts.baseUrl Required. The base client-server URL e.g.
* 'http://localhost:8008'.
* @param {Function} opts.request Required. The function to call for HTTP
* requests. This function must look like function(opts, callback){ ... }.
* @param {string} opts.prefix Required. The matrix client prefix to use, e.g.
* '/_matrix/client/r0'. See PREFIX_R0 and PREFIX_UNSTABLE for constants.
*
* @param {bool=} opts.onlyData True to return only the 'data' component of the
* response (e.g. the parsed HTTP body). If false, requests will return an
* object with the properties <tt>code</tt>, <tt>headers</tt> and <tt>data</tt>.
*
* @param {string} opts.accessToken The access_token to send with requests. Can be
* null to not send an access token.
* @param {Object=} opts.extraParams Optional. Extra query parameters to send on
* requests.
* @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"]);
opts.onlyData = opts.onlyData || false;
this.event_emitter = event_emitter;
this.opts = opts;
this.uploads = [];
};
module.exports.MatrixHttpApi.prototype = {
/**
* Get the content repository url with query parameters.
* @return {Object} An object with a 'base', 'path' and 'params' for base URL,
* path and query parameters respectively.
*/
getContentUri: function() {
const params = {
access_token: this.opts.accessToken,
};
return {
base: this.opts.baseUrl,
path: "/_matrix/media/v1/upload",
params: params,
};
},
/**
* Upload content to the Home Server
*
* @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, 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
// browser-request base64 encode and then decode it again, we just
// use XMLHttpRequest directly.
// (browser-request doesn't support progress either, which is also kind
// of important here)
const upload = { loaded: 0, total: 0 };
let promise;
// XMLHttpRequest doesn't parse JSON for us. request normally does, but
// we're setting opts.json=false so that it doesn't JSON-encode the
// request, which also means it doesn't JSON-decode the response. Either
// way, we have to JSON-parse the response ourselves.
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 resp;
try {
if (!xhr.responseText) {
throw new Error('No response body.');
}
resp = xhr.responseText;
if (bodyParser) {
resp = bodyParser(resp);
}
} catch (err) {
err.http_status = xhr.status;
cb(err);
return;
}
cb(undefined, xhr, resp);
break;
}
};
xhr.upload.addEventListener("progress", function(ev) {
callbacks.clearTimeout(xhr.timeout_timer);
upload.loaded = ev.loaded;
upload.total = ev.total;
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
defer.notify(ev);
});
let url = this.opts.baseUrl + "/_matrix/media/v1/upload";
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
url += "&filename=" + encodeURIComponent(fileName);
xhr.open("POST", url);
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 {
const queryParams = {
filename: fileName,
};
promise = this.authedRequest(
opts.callback, "POST", "/upload", queryParams, body, {
prefix: "/_matrix/media/v1",
headers: {"Content-Type": contentType},
json: false,
bodyParser: bodyParser,
},
);
}
const self = this;
// remove the upload from the list on completion
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;
}
}
});
// copy our dirty abort() method to the new promise
promise0.abort = promise.abort;
upload.promise = promise0;
this.uploads.push(upload);
return promise0;
},
cancelUpload: function(promise) {
if (promise.abort) {
promise.abort();
return true;
}
return false;
},
getCurrentUploads: function() {
return this.uploads;
},
idServerRequest: function(callback, method, path, params, prefix) {
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,
);
}
const opts = {
uri: fullUri,
method: method,
withCredentials: false,
json: false,
_matrix_opts: this.opts,
};
if (method == 'GET') {
opts.qs = params;
} else {
opts.form = params;
}
const defer = q.defer();
this.opts.request(
opts,
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
// parse it manually
return defer.promise.then(function(response) {
return JSON.parse(response);
});
},
/**
* Perform an authorised request to the homeserver.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
*
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
*
* @param {Object=} opts additional options
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
authedRequest: function(callback, method, path, queryParams, data, opts) {
if (!queryParams) {
queryParams = {};
}
if (!queryParams.access_token) {
queryParams.access_token = this.opts.accessToken;
}
const request_promise = this.request(
callback, method, path, queryParams, data, opts,
);
const self = this;
request_promise.catch(function(err) {
if (err.errcode == 'M_UNKNOWN_TOKEN') {
self.event_emitter.emit("Session.logged_out");
}
});
// return the original promise, otherwise tests break due to it having to
// go around the event loop one more time to process the result of the request
return request_promise;
},
/**
* Perform a request to the homeserver without any credentials.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
*
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
*
* @param {Object=} opts additional options
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
request: function(callback, method, path, queryParams, data, opts) {
opts = opts || {};
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,
);
},
/**
* Perform an authorised request to the homeserver with a specific path
* prefix which overrides the default for this call only. Useful for hitting
* different Matrix Client-Server versions.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*
* @deprecated prefer authedRequest with opts.prefix
*/
authedRequestWithPrefix: function(callback, method, path, queryParams, data,
prefix, localTimeoutMs) {
return this.authedRequest(
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
},
);
},
/**
* Perform a request to the homeserver without any credentials but with a
* specific path prefix which overrides the default for this call only.
* Useful for hitting different Matrix Client-Server versions.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*
* @deprecated prefer request with opts.prefix
*/
requestWithPrefix: function(callback, method, path, queryParams, data, prefix,
localTimeoutMs) {
return this.request(
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
},
);
},
/**
* Perform a request to an arbitrary URL.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} uri The HTTP URI
*
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
*
* @param {Object=} opts additional options
*
* @param {Number=} opts.localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
*
* @param {sting=} opts.prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha". If not specified, uses this.opts.prefix.
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*/
requestOtherUrl: function(callback, method, uri, queryParams, data,
opts) {
if (opts === undefined || opts === null) {
opts = {};
} else if (isFinite(opts)) {
// opts used to be localTimeoutMs
opts = {
localTimeoutMs: opts,
};
}
return this._request(
callback, method, uri, queryParams, data, opts,
);
},
/**
* Form and return a homeserver request URL based on the given path
* params and prefix.
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @return {string} URL
*/
getUrl: function(path, queryParams, prefix) {
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,
);
}
opts = opts || {};
const self = this;
if (this.opts.extraParams) {
for (const key in this.opts.extraParams) {
if (!this.opts.extraParams.hasOwnProperty(key)) {
continue;
}
queryParams[key] = this.opts.extraParams[key];
}
}
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;
if (req && req.abort) {
req.abort();
}
defer.reject(new module.exports.MatrixError({
error: "Locally timed out waiting for a response",
errcode: "ORG.MATRIX.JSSDK_TIMEOUT",
timeout: localTimeoutMs,
}));
}, localTimeoutMs);
}
const reqPromise = defer.promise;
try {
req = this.opts.request(
{
uri: uri,
method: method,
withCredentials: false,
qs: queryParams,
body: data,
json: json,
timeout: localTimeoutMs,
headers: opts.headers || {},
_matrix_opts: this.opts,
},
function(err, response, body) {
if (localTimeoutMs) {
callbacks.clearTimeout(timeoutId);
if (timedOut) {
return; // already rejected promise
}
}
// if json is falsy, we won't parse any error response, so need
// to do so before turning it into a MatrixError
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) {
defer.reject(ex);
if (callback) {
callback(ex);
}
}
return reqPromise;
},
};
/*
* Returns a callback that can be invoked by an HTTP request on completion,
* that will either resolve or reject the given defer as well as invoke the
* given userDefinedCallback (if any).
*
* 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.
*
*/
const requestCallback = function(
defer, userDefinedCallback, onlyData,
parseErrorJson, bodyParser,
) {
userDefinedCallback = userDefinedCallback || function() {};
return function(err, response, body) {
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 {
const res = {
code: response.statusCode,
headers: response.headers,
data: body,
};
defer.resolve(onlyData ? body : res);
userDefinedCallback(null, onlyData ? body : res);
}
};
};
/**
* Construct a Matrix error. This is a JavaScript Error with additional
* information specific to the standard Matrix error response.
* @constructor
* @param {Object} errorJson The Matrix error JSON returned from the homeserver.
* @prop {string} errcode The Matrix 'errcode' value, e.g. "M_FORBIDDEN".
* @prop {string} name Same as MatrixError.errcode but with a default unknown string.
* @prop {string} message The Matrix 'error' value, e.g. "Missing token."
* @prop {Object} data The raw Matrix error JSON used to construct this object.
* @prop {integer} httpStatus The numeric HTTP status code given
*/
module.exports.MatrixError = function MatrixError(errorJson) {
errorJson = errorJson || {};
this.errcode = errorJson.errcode;
this.name = errorJson.errcode || "Unknown error code";
this.message = errorJson.error || "Unknown message";
this.data = errorJson;
};
module.exports.MatrixError.prototype = Object.create(Error.prototype);
/** */
module.exports.MatrixError.prototype.constructor = module.exports.MatrixError;
+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;
+66 -8
View File
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/** The {@link module:models/event.MatrixEvent|MatrixEvent} class. */
@@ -6,18 +21,19 @@ module.exports.MatrixEvent = require("./models/event").MatrixEvent;
module.exports.EventStatus = require("./models/event").EventStatus;
/** The {@link module:store/memory.MatrixInMemoryStore|MatrixInMemoryStore} class. */
module.exports.MatrixInMemoryStore = require("./store/memory").MatrixInMemoryStore;
/** The {@link module:store/webstorage~WebStorageStore|WebStorageStore} class.
* <strong>Work in progress; unstable.</strong> */
module.exports.WebStorageStore = require("./store/webstorage");
/** The {@link module:http-api.MatrixHttpApi|MatrixHttpApi} class. */
module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi;
/** The {@link module:http-api.MatrixError|MatrixError} class. */
module.exports.MatrixError = require("./http-api").MatrixError;
/** The {@link module:client.MatrixClient|MatrixClient} class. */
module.exports.MatrixClient = require("./client").MatrixClient;
/** The {@link module:models/room~Room|Room} class. */
/** The {@link module:models/room|Room} class. */
module.exports.Room = require("./models/room");
/** The {@link module:models/room-member~RoomMember|RoomMember} class. */
/** The {@link module:models/event-timeline~EventTimeline} class. */
module.exports.EventTimeline = require("./models/event-timeline");
/** The {@link module:models/event-timeline-set~EventTimelineSet} class. */
module.exports.EventTimelineSet = require("./models/event-timeline-set");
/** The {@link module:models/room-member|RoomMember} class. */
module.exports.RoomMember = require("./models/room-member");
/** The {@link module:models/room-state~RoomState|RoomState} class. */
module.exports.RoomState = require("./models/room-state");
@@ -30,6 +46,15 @@ module.exports.MatrixScheduler = require("./scheduler");
module.exports.WebStorageSessionStore = require("./store/session/webstorage");
/** True if crypto libraries are being used on this client. */
module.exports.CRYPTO_ENABLED = require("./client").CRYPTO_ENABLED;
/** {@link module:content-repo|ContentRepo} utility functions. */
module.exports.ContentRepo = require("./content-repo");
/** The {@link module:filter~Filter|Filter} class. */
module.exports.Filter = require("./filter");
/** The {@link module:timeline-window~TimelineWindow} class. */
module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
/** The {@link module:interactive-auth} class. */
module.exports.InteractiveAuth = require("./interactive-auth");
/**
* Create a new Matrix Call.
@@ -43,7 +68,7 @@ module.exports.createNewMatrixCall = require("./webrtc/call").createNewMatrixCal
// expose the underlying request object so different environments can use
// different request libs (e.g. request or browser-request)
var request;
let request;
/**
* The function used to perform HTTP requests. Only use this if you want to
* use a different HTTP library, e.g. Angular's <code>$http</code>. This should
@@ -54,6 +79,27 @@ module.exports.request = function(r) {
request = r;
};
/**
* Return the currently-set request function.
* @return {requestFunction} The current request function.
*/
module.exports.getRequest = function() {
return request;
};
/**
* Apply wrapping code around the request function. The wrapper function is
* installed as the new request handler, and when invoked it is passed the
* previous value, along with the options and callback arguments.
* @param {requestWrapperFunction} wrapper The wrapping function.
*/
module.exports.wrapRequest = function(wrapper) {
const origRequest = request;
request = function(options, callback) {
return wrapper(origRequest, options, callback);
};
};
/**
* Construct a Matrix Client. Similar to {@link module:client~MatrixClient}
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
@@ -73,11 +119,13 @@ module.exports.request = function(r) {
module.exports.createClient = function(opts) {
if (typeof opts === "string") {
opts = {
"baseUrl": opts
"baseUrl": opts,
};
}
opts.request = opts.request || request;
opts.store = opts.store || new module.exports.MatrixInMemoryStore();
opts.store = opts.store || new module.exports.MatrixInMemoryStore({
localStorage: global.localStorage,
});
opts.scheduler = opts.scheduler || new module.exports.MatrixScheduler();
return new module.exports.MatrixClient(opts);
};
@@ -99,6 +147,16 @@ module.exports.createClient = function(opts) {
* @param {requestCallback} callback The request callback.
*/
/**
* A wrapper for the request function interface.
* @callback requestWrapperFunction
* @param {requestFunction} origRequest The underlying request function being
* wrapped
* @param {Object} opts The options for this HTTP request, given in the same
* form as {@link requestFunction}.
* @param {requestCallback} callback The request callback.
*/
/**
* The request callback interface for performing HTTP requests. This matches the
* API for the {@link https://github.com/request/request#requestoptions-callback|
+119
View File
@@ -0,0 +1,119 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/event-context
*/
/**
* Construct a new EventContext
*
* An eventcontext is used for circumstances such as search results, when we
* have a particular event of interest, and a bunch of events before and after
* it.
*
* It also stores pagination tokens for going backwards and forwards in the
* timeline.
*
* @param {MatrixEvent} ourEvent the event at the centre of this context
*
* @constructor
*/
function EventContext(ourEvent) {
this._timeline = [ourEvent];
this._ourEventIndex = 0;
this._paginateTokens = {b: null, f: null};
// this is used by MatrixClient to keep track of active requests
this._paginateRequests = {b: null, f: null};
}
/**
* Get the main event of interest
*
* This is a convenience function for getTimeline()[getOurEventIndex()].
*
* @return {MatrixEvent} The event at the centre of this context.
*/
EventContext.prototype.getEvent = function() {
return this._timeline[this._ourEventIndex];
};
/**
* Get the list of events in this context
*
* @return {Array} An array of MatrixEvents
*/
EventContext.prototype.getTimeline = function() {
return this._timeline;
};
/**
* Get the index in the timeline of our event
*
* @return {Number}
*/
EventContext.prototype.getOurEventIndex = function() {
return this._ourEventIndex;
};
/**
* Get a pagination token.
*
* @param {boolean} backwards true to get the pagination token for going
* backwards in time
* @return {string}
*/
EventContext.prototype.getPaginateToken = function(backwards) {
return this._paginateTokens[backwards ? 'b' : 'f'];
};
/**
* Set a pagination token.
*
* Generally this will be used only by the matrix js sdk.
*
* @param {string} token pagination token
* @param {boolean} backwards true to set the pagination token for going
* backwards in time
*/
EventContext.prototype.setPaginateToken = function(token, backwards) {
this._paginateTokens[backwards ? 'b' : 'f'] = token;
};
/**
* Add more events to the timeline
*
* @param {Array} events new events, in timeline order
* @param {boolean} atStart true to insert new events at the start
*/
EventContext.prototype.addEvents = function(events, atStart) {
// TODO: should we share logic with Room.addEventsToTimeline?
// Should Room even use EventContext?
if (atStart) {
this._timeline = events.concat(this._timeline);
this._ourEventIndex += events.length;
} else {
this._timeline = this._timeline.concat(events);
}
};
/**
* The EventContext class
*/
module.exports = EventContext;
+658
View File
@@ -0,0 +1,658 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/event-timeline-set
*/
const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
const EventTimeline = require("./event-timeline");
// var DEBUG = false;
const DEBUG = true;
let debuglog;
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
debuglog = console.log.bind(console);
} else {
debuglog = function() {};
}
/**
* Construct a set of EventTimeline objects, typically on behalf of a given
* room. A room may have multiple EventTimelineSets for different levels
* of filtering. The global notification list is also an EventTimelineSet, but
* lacks a room.
*
* <p>This is an ordered sequence of timelines, which may or may not
* be continuous. Each timeline lists a series of events, as well as tracking
* the room state at the start and the end of the timeline (if appropriate).
* It also tracks forward and backward pagination tokens, as well as containing
* links to the next timeline in the sequence.
*
* <p>There is one special timeline - the 'live' timeline, which represents the
* timeline to which events are being added in real-time as they are received
* from the /sync API. Note that you should not retain references to this
* timeline - even if it is the current timeline right now, it may not remain
* so if the server gives us a timeline gap in /sync.
*
* <p>In order that we can find events from their ids later, we also maintain a
* map from event_id to timeline and index.
*
* @constructor
* @param {?Room} room the optional room for this timelineSet
* @param {Object} opts hash of options inherited from Room.
* opts.timelineSupport gives whether timeline support is enabled
* opts.filter is the filter object, if any, for this timelineSet.
*/
function EventTimelineSet(room, opts) {
this.room = room;
this._timelineSupport = Boolean(opts.timelineSupport);
this._liveTimeline = new EventTimeline(this);
// just a list - *not* ordered.
this._timelines = [this._liveTimeline];
this._eventIdToTimeline = {};
this._filter = opts.filter || null;
}
utils.inherits(EventTimelineSet, EventEmitter);
/**
* Get the filter object this timeline set is filtered on, if any
* @return {?Filter} the optional filter for this timelineSet
*/
EventTimelineSet.prototype.getFilter = function() {
return this._filter;
};
/**
* Set the filter object this timeline set is filtered on
* (passed to the server when paginating via /messages).
* @param {Filter} filter the filter for this timelineSet
*/
EventTimelineSet.prototype.setFilter = function(filter) {
this._filter = filter;
};
/**
* Get the list of pending sent events for this timelineSet's room, filtered
* by the timelineSet's filter if appropriate.
*
* @return {module:models/event.MatrixEvent[]} A list of the sent events
* waiting for remote echo.
*
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
*/
EventTimelineSet.prototype.getPendingEvents = function() {
if (!this.room) {
return [];
}
if (this._filter) {
return this._filter.filterRoomTimeline(this.room.getPendingEvents());
} else {
return this.room.getPendingEvents();
}
};
/**
* Get the live timeline for this room.
*
* @return {module:models/event-timeline~EventTimeline} live timeline
*/
EventTimelineSet.prototype.getLiveTimeline = function() {
return this._liveTimeline;
};
/**
* Return the timeline (if any) this event is in.
* @param {String} eventId the eventId being sought
* @return {module:models/event-timeline~EventTimeline} timeline
*/
EventTimelineSet.prototype.eventIdToTimeline = function(eventId) {
return this._eventIdToTimeline[eventId];
};
/**
* Track a new event as if it were in the same timeline as an old event,
* replacing it.
* @param {String} oldEventId event ID of the original event
* @param {String} newEventId event ID of the replacement event
*/
EventTimelineSet.prototype.replaceEventId = function(oldEventId, newEventId) {
const existingTimeline = this._eventIdToTimeline[oldEventId];
if (existingTimeline) {
delete this._eventIdToTimeline[oldEventId];
this._eventIdToTimeline[newEventId] = existingTimeline;
}
};
/**
* Reset the live timeline, and start a new one.
*
* <p>This is used when /sync returns a 'limited' timeline.
*
* @param {string=} backPaginationToken token for back-paginating the new timeline
* @param {?bool} flush Whether to flush the non-live timelines too.
*
* @fires module:client~MatrixClient#event:"Room.timelineReset"
*/
EventTimelineSet.prototype.resetLiveTimeline = function(backPaginationToken, flush) {
let newTimeline;
if (!this._timelineSupport || flush) {
// if timeline support is disabled, forget about the old timelines
newTimeline = new EventTimeline(this);
this._timelines = [newTimeline];
this._eventIdToTimeline = {};
} else {
newTimeline = this.addTimeline();
}
// initialise the state in the new timeline from our last known state
const evMap = this._liveTimeline.getState(EventTimeline.FORWARDS).events;
const events = [];
for (const evtype in evMap) {
if (!evMap.hasOwnProperty(evtype)) {
continue;
}
for (const stateKey in evMap[evtype]) {
if (!evMap[evtype].hasOwnProperty(stateKey)) {
continue;
}
events.push(evMap[evtype][stateKey]);
}
}
newTimeline.initialiseState(events);
// make sure we set the pagination token before firing timelineReset,
// otherwise clients which start back-paginating will fail, and then get
// stuck without realising that they *can* back-paginate.
newTimeline.setPaginationToken(backPaginationToken, EventTimeline.BACKWARDS);
this._liveTimeline = newTimeline;
this.emit("Room.timelineReset", this.room, this);
};
/**
* Get the timeline which contains the given event, if any
*
* @param {string} eventId event ID to look for
* @return {?module:models/event-timeline~EventTimeline} timeline containing
* the given event, or null if unknown
*/
EventTimelineSet.prototype.getTimelineForEvent = function(eventId) {
const res = this._eventIdToTimeline[eventId];
return (res === undefined) ? null : res;
};
/**
* Get an event which is stored in our timelines
*
* @param {string} eventId event ID to look for
* @return {?module:models/event~MatrixEvent} the given event, or undefined if unknown
*/
EventTimelineSet.prototype.findEventById = function(eventId) {
const tl = this.getTimelineForEvent(eventId);
if (!tl) {
return undefined;
}
return utils.findElement(tl.getEvents(), function(ev) {
return ev.getId() == eventId;
});
};
/**
* Add a new timeline to this timeline list
*
* @return {module:models/event-timeline~EventTimeline} newly-created timeline
*/
EventTimelineSet.prototype.addTimeline = function() {
if (!this._timelineSupport) {
throw new Error("timeline support is disabled. Set the 'timelineSupport'" +
" parameter to true when creating MatrixClient to enable" +
" it.");
}
const timeline = new EventTimeline(this);
this._timelines.push(timeline);
return timeline;
};
/**
* Add events to a timeline
*
* <p>Will fire "Room.timeline" for each event added.
*
* @param {MatrixEvent[]} events A list of events to add.
*
* @param {boolean} toStartOfTimeline True to add these events to the start
* (oldest) instead of the end (newest) of the timeline. If true, the oldest
* event will be the <b>last</b> element of 'events'.
*
* @param {module:models/event-timeline~EventTimeline} timeline timeline to
* add events to.
*
* @param {string=} paginationToken token for the next batch of events
*
* @fires module:client~MatrixClient#event:"Room.timeline"
*
*/
EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
timeline, paginationToken) {
if (!timeline) {
throw new Error(
"'timeline' not specified for EventTimelineSet.addEventsToTimeline",
);
}
if (!toStartOfTimeline && timeline == this._liveTimeline) {
throw new Error(
"EventTimelineSet.addEventsToTimeline cannot be used for adding events to " +
"the live timeline - use Room.addLiveEvents instead",
);
}
if (this._filter) {
events = this._filter.filterRoomTimeline(events);
if (!events.length) {
return;
}
}
const direction = toStartOfTimeline ? EventTimeline.BACKWARDS :
EventTimeline.FORWARDS;
const inverseDirection = toStartOfTimeline ? EventTimeline.FORWARDS :
EventTimeline.BACKWARDS;
// Adding events to timelines can be quite complicated. The following
// illustrates some of the corner-cases.
//
// Let's say we start by knowing about four timelines. timeline3 and
// timeline4 are neighbours:
//
// timeline1 timeline2 timeline3 timeline4
// [M] [P] [S] <------> [T]
//
// Now we paginate timeline1, and get the following events from the server:
// [M, N, P, R, S, T, U].
//
// 1. First, we ignore event M, since we already know about it.
//
// 2. Next, we append N to timeline 1.
//
// 3. Next, we don't add event P, since we already know about it,
// but we do link together the timelines. We now have:
//
// timeline1 timeline2 timeline3 timeline4
// [M, N] <---> [P] [S] <------> [T]
//
// 4. Now we add event R to timeline2:
//
// timeline1 timeline2 timeline3 timeline4
// [M, N] <---> [P, R] [S] <------> [T]
//
// Note that we have switched the timeline we are working on from
// timeline1 to timeline2.
//
// 5. We ignore event S, but again join the timelines:
//
// timeline1 timeline2 timeline3 timeline4
// [M, N] <---> [P, R] <---> [S] <------> [T]
//
// 6. We ignore event T, and the timelines are already joined, so there
// is nothing to do.
//
// 7. Finally, we add event U to timeline4:
//
// timeline1 timeline2 timeline3 timeline4
// [M, N] <---> [P, R] <---> [S] <------> [T, U]
//
// The important thing to note in the above is what happened when we
// already knew about a given event:
//
// - if it was appropriate, we joined up the timelines (steps 3, 5).
// - in any case, we started adding further events to the timeline which
// contained the event we knew about (steps 3, 5, 6).
//
//
// So much for adding events to the timeline. But what do we want to do
// with the pagination token?
//
// In the case above, we will be given a pagination token which tells us how to
// get events beyond 'U' - in this case, it makes sense to store this
// against timeline4. But what if timeline4 already had 'U' and beyond? in
// that case, our best bet is to throw away the pagination token we were
// given and stick with whatever token timeline4 had previously. In short,
// we want to only store the pagination token if the last event we receive
// is one we didn't previously know about.
//
// We make an exception for this if it turns out that we already knew about
// *all* of the events, and we weren't able to join up any timelines. When
// that happens, it means our existing pagination token is faulty, since it
// is only telling us what we already know. Rather than repeatedly
// paginating with the same token, we might as well use the new pagination
// token in the hope that we eventually work our way out of the mess.
let didUpdate = false;
let lastEventWasNew = false;
for (let i = 0; i < events.length; i++) {
const event = events[i];
const eventId = event.getId();
const existingTimeline = this._eventIdToTimeline[eventId];
if (!existingTimeline) {
// we don't know about this event yet. Just add it to the timeline.
this.addEventToTimeline(event, timeline, toStartOfTimeline);
lastEventWasNew = true;
didUpdate = true;
continue;
}
lastEventWasNew = false;
if (existingTimeline == timeline) {
debuglog("Event " + eventId + " already in timeline " + timeline);
continue;
}
const neighbour = timeline.getNeighbouringTimeline(direction);
if (neighbour) {
// this timeline already has a neighbour in the relevant direction;
// let's assume the timelines are already correctly linked up, and
// skip over to it.
//
// there's probably some edge-case here where we end up with an
// event which is in a timeline a way down the chain, and there is
// a break in the chain somewhere. But I can't really imagine how
// that would happen, so I'm going to ignore it for now.
//
if (existingTimeline == neighbour) {
debuglog("Event " + eventId + " in neighbouring timeline - " +
"switching to " + existingTimeline);
} else {
debuglog("Event " + eventId + " already in a different " +
"timeline " + existingTimeline);
}
timeline = existingTimeline;
continue;
}
// time to join the timelines.
console.info("Already have timeline for " + eventId +
" - joining timeline " + timeline + " to " +
existingTimeline);
timeline.setNeighbouringTimeline(existingTimeline, direction);
existingTimeline.setNeighbouringTimeline(timeline, inverseDirection);
timeline = existingTimeline;
didUpdate = true;
}
// see above - if the last event was new to us, or if we didn't find any
// new information, we update the pagination token for whatever
// timeline we ended up on.
if (lastEventWasNew || !didUpdate) {
timeline.setPaginationToken(paginationToken, direction);
}
};
/**
* Add an event to the end of this live timeline.
*
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
*/
EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
if (this._filter) {
const events = this._filter.filterRoomTimeline([event]);
if (!events.length) {
return;
}
}
const timeline = this._eventIdToTimeline[event.getId()];
if (timeline) {
if (duplicateStrategy === "replace") {
debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " +
event.getId());
const tlEvents = timeline.getEvents();
for (let j = 0; j < tlEvents.length; j++) {
if (tlEvents[j].getId() === event.getId()) {
// still need to set the right metadata on this event
EventTimeline.setEventMetadata(
event,
timeline.getState(EventTimeline.FORWARDS),
false,
);
if (!tlEvents[j].encryptedType) {
tlEvents[j] = event;
}
// XXX: we need to fire an event when this happens.
break;
}
}
} else {
debuglog("EventTimelineSet.addLiveEvent: ignoring duplicate event " +
event.getId());
}
return;
}
this.addEventToTimeline(event, this._liveTimeline, false);
};
/**
* Add event to the given timeline, and emit Room.timeline. Assumes
* we have already checked we don't know about this event.
*
* Will fire "Room.timeline" for each event added.
*
* @param {MatrixEvent} event
* @param {EventTimeline} timeline
* @param {boolean} toStartOfTimeline
*
* @fires module:client~MatrixClient#event:"Room.timeline"
*/
EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
toStartOfTimeline) {
const eventId = event.getId();
timeline.addEvent(event, toStartOfTimeline);
this._eventIdToTimeline[eventId] = timeline;
const data = {
timeline: timeline,
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
};
this.emit("Room.timeline", event, this.room,
Boolean(toStartOfTimeline), false, data);
};
/**
* Replaces event with ID oldEventId with one with newEventId, if oldEventId is
* recognised. Otherwise, add to the live timeline. Used to handle remote echos.
*
* @param {MatrixEvent} localEvent the new event to be added to the timeline
* @param {String} oldEventId the ID of the original event
* @param {boolean} newEventId the ID of the replacement event
*
* @fires module:client~MatrixClient#event:"Room.timeline"
*/
EventTimelineSet.prototype.handleRemoteEcho = function(localEvent, oldEventId,
newEventId) {
// XXX: why don't we infer newEventId from localEvent?
const existingTimeline = this._eventIdToTimeline[oldEventId];
if (existingTimeline) {
delete this._eventIdToTimeline[oldEventId];
this._eventIdToTimeline[newEventId] = existingTimeline;
} else {
if (this._filter) {
if (this._filter.filterRoomTimeline([localEvent]).length) {
this.addEventToTimeline(localEvent, this._liveTimeline, false);
}
} else {
this.addEventToTimeline(localEvent, this._liveTimeline, false);
}
}
};
/**
* Removes a single event from this room.
*
* @param {String} eventId The id of the event to remove
*
* @return {?MatrixEvent} the removed event, or null if the event was not found
* in this room.
*/
EventTimelineSet.prototype.removeEvent = function(eventId) {
const timeline = this._eventIdToTimeline[eventId];
if (!timeline) {
return null;
}
const removed = timeline.removeEvent(eventId);
if (removed) {
delete this._eventIdToTimeline[eventId];
const data = {
timeline: timeline,
};
this.emit("Room.timeline", removed, this.room, undefined, true, data);
}
return removed;
};
/**
* Determine where two events appear in the timeline relative to one another
*
* @param {string} eventId1 The id of the first event
* @param {string} eventId2 The id of the second event
* @return {?number} a number less than zero if eventId1 precedes eventId2, and
* greater than zero if eventId1 succeeds eventId2. zero if they are the
* same event; null if we can't tell (either because we don't know about one
* of the events, or because they are in separate timelines which don't join
* up).
*/
EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
if (eventId1 == eventId2) {
// optimise this case
return 0;
}
const timeline1 = this._eventIdToTimeline[eventId1];
const timeline2 = this._eventIdToTimeline[eventId2];
if (timeline1 === undefined) {
return null;
}
if (timeline2 === undefined) {
return null;
}
if (timeline1 === timeline2) {
// both events are in the same timeline - figure out their
// relative indices
let idx1, idx2;
const events = timeline1.getEvents();
for (let idx = 0; idx < events.length &&
(idx1 === undefined || idx2 === undefined); idx++) {
const evId = events[idx].getId();
if (evId == eventId1) {
idx1 = idx;
}
if (evId == eventId2) {
idx2 = idx;
}
}
return idx1 - idx2;
}
// the events are in different timelines. Iterate through the
// linkedlist to see which comes first.
// first work forwards from timeline1
let tl = timeline1;
while (tl) {
if (tl === timeline2) {
// timeline1 is before timeline2
return -1;
}
tl = tl.getNeighbouringTimeline(EventTimeline.FORWARDS);
}
// now try backwards from timeline1
tl = timeline1;
while (tl) {
if (tl === timeline2) {
// timeline2 is before timeline1
return 1;
}
tl = tl.getNeighbouringTimeline(EventTimeline.BACKWARDS);
}
// the timelines are not contiguous.
return null;
};
/**
* The EventTimelineSet class.
*/
module.exports = EventTimelineSet;
/**
* Fires whenever the timeline in a room is updated.
* @event module:client~MatrixClient#"Room.timeline"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {?Room} room The room, if any, whose timeline was updated.
* @param {boolean} toStartOfTimeline True if this event was added to the start
* @param {boolean} removed True if this event has just been removed from the timeline
* (beginning; oldest) of the timeline e.g. due to pagination.
*
* @param {object} data more data about the event
*
* @param {module:event-timeline.EventTimeline} data.timeline the timeline the
* event was added to/removed from
*
* @param {boolean} data.liveEvent true if the event was a real-time event
* added to the end of the live timeline
*
* @example
* matrixClient.on("Room.timeline",
* function(event, room, toStartOfTimeline, removed, data) {
* if (!toStartOfTimeline && data.liveEvent) {
* var messageToAppend = room.timeline.[room.timeline.length - 1];
* }
* });
*/
/**
* Fires whenever the live timeline in a room is reset.
*
* When we get a 'limited' sync (for example, after a network outage), we reset
* the live timeline to be empty before adding the recent events to the new
* timeline. This event is fired after the timeline is reset, and before the
* new events are added.
*
* @event module:client~MatrixClient#"Room.timelineReset"
* @param {Room} room The room whose live timeline was reset, if any
* @param {EventTimelineSet} timelineSet timelineSet room whose live timeline was reset
*/
+342
View File
@@ -0,0 +1,342 @@
"use strict";
/**
* @module models/event-timeline
*/
const RoomState = require("./room-state");
const utils = require("../utils");
const MatrixEvent = require("./event").MatrixEvent;
/**
* Construct a new EventTimeline
*
* <p>An EventTimeline represents a contiguous sequence of events in a room.
*
* <p>As well as keeping track of the events themselves, it stores the state of
* the room at the beginning and end of the timeline, and pagination tokens for
* going backwards and forwards in the timeline.
*
* <p>In order that clients can meaningfully maintain an index into a timeline,
* the EventTimeline object tracks a 'baseIndex'. This starts at zero, but is
* incremented when events are prepended to the timeline. The index of an event
* relative to baseIndex therefore remains constant.
*
* <p>Once a timeline joins up with its neighbour, they are linked together into a
* doubly-linked list.
*
* @param {EventTimelineSet} eventTimelineSet the set of timelines this is part of
* @constructor
*/
function EventTimeline(eventTimelineSet) {
this._eventTimelineSet = eventTimelineSet;
this._roomId = eventTimelineSet.room ? eventTimelineSet.room.roomId : null;
this._events = [];
this._baseIndex = 0;
this._startState = new RoomState(this._roomId);
this._startState.paginationToken = null;
this._endState = new RoomState(this._roomId);
this._endState.paginationToken = null;
this._prevTimeline = null;
this._nextTimeline = null;
// this is used by client.js
this._paginationRequests = {'b': null, 'f': null};
this._name = this._roomId + ":" + new Date().toISOString();
}
/**
* Symbolic constant for methods which take a 'direction' argument:
* refers to the start of the timeline, or backwards in time.
*/
EventTimeline.BACKWARDS = "b";
/**
* Symbolic constant for methods which take a 'direction' argument:
* refers to the end of the timeline, or forwards in time.
*/
EventTimeline.FORWARDS = "f";
/**
* Initialise the start and end state with the given events
*
* <p>This can only be called before any events are added.
*
* @param {MatrixEvent[]} stateEvents list of state events to initialise the
* state with.
* @throws {Error} if an attempt is made to call this after addEvent is called.
*/
EventTimeline.prototype.initialiseState = function(stateEvents) {
if (this._events.length > 0) {
throw new Error("Cannot initialise state after events are added");
}
// we deep-copy the events here, in case they get changed later - we don't
// want changes to the start state leaking through to the end state.
const oldStateEvents = utils.map(
utils.deepCopy(
stateEvents.map(function(mxEvent) {
return mxEvent.event;
}),
),
function(ev) {
return new MatrixEvent(ev);
});
this._startState.setStateEvents(oldStateEvents);
this._endState.setStateEvents(stateEvents);
};
/**
* Get the ID of the room for this timeline
* @return {string} room ID
*/
EventTimeline.prototype.getRoomId = function() {
return this._roomId;
};
/**
* Get the filter for this timeline's timelineSet (if any)
* @return {Filter} filter
*/
EventTimeline.prototype.getFilter = function() {
return this._eventTimelineSet.getFilter();
};
/**
* Get the timelineSet for this timeline
* @return {EventTimelineSet} timelineSet
*/
EventTimeline.prototype.getTimelineSet = function() {
return this._eventTimelineSet;
};
/**
* Get the base index.
*
* <p>This is an index which is incremented when events are prepended to the
* timeline. An individual event therefore stays at the same index in the array
* relative to the base index (although note that a given event's index may
* well be less than the base index, thus giving that event a negative relative
* index).
*
* @return {number}
*/
EventTimeline.prototype.getBaseIndex = function() {
return this._baseIndex;
};
/**
* Get the list of events in this context
*
* @return {MatrixEvent[]} An array of MatrixEvents
*/
EventTimeline.prototype.getEvents = function() {
return this._events;
};
/**
* Get the room state at the start/end of the timeline
*
* @param {string} direction EventTimeline.BACKWARDS to get the state at the
* start of the timeline; EventTimeline.FORWARDS to get the state at the end
* of the timeline.
*
* @return {RoomState} state at the start/end of the timeline
*/
EventTimeline.prototype.getState = function(direction) {
if (direction == EventTimeline.BACKWARDS) {
return this._startState;
} else if (direction == EventTimeline.FORWARDS) {
return this._endState;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
};
/**
* Get a pagination token
*
* @param {string} direction EventTimeline.BACKWARDS to get the pagination
* token for going backwards in time; EventTimeline.FORWARDS to get the
* pagination token for going forwards in time.
*
* @return {?string} pagination token
*/
EventTimeline.prototype.getPaginationToken = function(direction) {
return this.getState(direction).paginationToken;
};
/**
* Set a pagination token
*
* @param {?string} token pagination token
*
* @param {string} direction EventTimeline.BACKWARDS to set the pagination
* token for going backwards in time; EventTimeline.FORWARDS to set the
* pagination token for going forwards in time.
*/
EventTimeline.prototype.setPaginationToken = function(token, direction) {
this.getState(direction).paginationToken = token;
};
/**
* Get the next timeline in the series
*
* @param {string} direction EventTimeline.BACKWARDS to get the previous
* timeline; EventTimeline.FORWARDS to get the next timeline.
*
* @return {?EventTimeline} previous or following timeline, if they have been
* joined up.
*/
EventTimeline.prototype.getNeighbouringTimeline = function(direction) {
if (direction == EventTimeline.BACKWARDS) {
return this._prevTimeline;
} else if (direction == EventTimeline.FORWARDS) {
return this._nextTimeline;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
};
/**
* Set the next timeline in the series
*
* @param {EventTimeline} neighbour previous/following timeline
*
* @param {string} direction EventTimeline.BACKWARDS to set the previous
* timeline; EventTimeline.FORWARDS to set the next timeline.
*
* @throws {Error} if an attempt is made to set the neighbouring timeline when
* it is already set.
*/
EventTimeline.prototype.setNeighbouringTimeline = function(neighbour, direction) {
if (this.getNeighbouringTimeline(direction)) {
throw new Error("timeline already has a neighbouring timeline - " +
"cannot reset neighbour");
}
if (direction == EventTimeline.BACKWARDS) {
this._prevTimeline = neighbour;
} else if (direction == EventTimeline.FORWARDS) {
this._nextTimeline = neighbour;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
// make sure we don't try to paginate this timeline
this.setPaginationToken(null, direction);
};
/**
* Add a new event to the timeline, and update the state
*
* @param {MatrixEvent} event new event
* @param {boolean} atStart true to insert new event at the start
*/
EventTimeline.prototype.addEvent = function(event, atStart) {
const stateContext = atStart ? this._startState : this._endState;
// only call setEventMetadata on the unfiltered timelineSets
const timelineSet = this.getTimelineSet();
if (timelineSet.room &&
timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
EventTimeline.setEventMetadata(event, stateContext, atStart);
// modify state
if (event.isState()) {
stateContext.setStateEvents([event]);
// it is possible that the act of setting the state event means we
// can set more metadata (specifically sender/target props), so try
// it again if the prop wasn't previously set. It may also mean that
// the sender/target is updated (if the event set was a room member event)
// so we want to use the *updated* member (new avatar/name) instead.
//
// However, we do NOT want to do this on member events if we're going
// back in time, else we'll set the .sender value for BEFORE the given
// member event, whereas we want to set the .sender value for the ACTUAL
// member event itself.
if (!event.sender || (event.getType() === "m.room.member" && !atStart)) {
EventTimeline.setEventMetadata(event, stateContext, atStart);
}
}
}
let insertIndex;
if (atStart) {
insertIndex = 0;
} else {
insertIndex = this._events.length;
}
this._events.splice(insertIndex, 0, event); // insert element
if (atStart) {
this._baseIndex++;
}
};
/**
* Static helper method to set sender and target properties
*
* @param {MatrixEvent} event the event whose metadata is to be set
* @param {RoomState} stateContext the room state to be queried
* @param {bool} toStartOfTimeline if true the event's forwardLooking flag is set false
*/
EventTimeline.setEventMetadata = function(event, stateContext, toStartOfTimeline) {
// set sender and target properties
event.sender = stateContext.getSentinelMember(
event.getSender(),
);
if (event.getType() === "m.room.member") {
event.target = stateContext.getSentinelMember(
event.getStateKey(),
);
}
if (event.isState()) {
// room state has no concept of 'old' or 'current', but we want the
// room state to regress back to previous values if toStartOfTimeline
// is set, which means inspecting prev_content if it exists. This
// is done by toggling the forwardLooking flag.
if (toStartOfTimeline) {
event.forwardLooking = false;
}
}
};
/**
* Remove an event from the timeline
*
* @param {string} eventId ID of event to be removed
* @return {?MatrixEvent} removed event, or null if not found
*/
EventTimeline.prototype.removeEvent = function(eventId) {
for (let i = this._events.length - 1; i >= 0; i--) {
const ev = this._events[i];
if (ev.getId() == eventId) {
this._events.splice(i, 1);
if (i < this._baseIndex) {
this._baseIndex--;
}
return ev;
}
}
return null;
};
/**
* Return a string to identify this timeline, for debugging
*
* @return {string} name for this timeline
*/
EventTimeline.prototype.toString = function() {
return this._name;
};
/**
* The EventTimeline class
*/
module.exports = EventTimeline;
+456
View File
@@ -0,0 +1,456 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module. See {@link MatrixEvent} and {@link RoomEvent} for
* the public classes.
* @module models/event
*/
const EventEmitter = require("events").EventEmitter;
const utils = require('../utils.js');
/**
* Enum for event statuses.
* @readonly
* @enum {string}
*/
module.exports.EventStatus = {
/** The event was not sent and will no longer be retried. */
NOT_SENT: "not_sent",
/** The message is being encrypted */
ENCRYPTING: "encrypting",
/** The event is in the process of being sent. */
SENDING: "sending",
/** The event is in a queue waiting to be sent. */
QUEUED: "queued",
/** The event has been sent to the server, but we have not yet received the
* echo. */
SENT: "sent",
/** The event was cancelled before it was successfully sent. */
CANCELLED: "cancelled",
};
/**
* Construct a Matrix Event object
* @constructor
*
* @param {Object} event The raw event to be wrapped in this DAO
*
* @prop {Object} event The raw (possibly encrypted) event. <b>Do not access
* this property</b> directly unless you absolutely have to. Prefer the getter
* methods defined on this class. Using the getter methods shields your app
* from changes to event JSON between Matrix versions.
*
* @prop {RoomMember} sender The room member who sent this event, or null e.g.
* this is a presence event.
* @prop {RoomMember} target The room member who is the target of this event, e.g.
* the invitee, the person being banned, etc.
* @prop {EventStatus} status The sending status of the event.
* @prop {Error} error most recent error associated with sending the event, if any
* @prop {boolean} forwardLooking True if this event is 'forward looking', meaning
* that getDirectionalContent() will return event.content and not event.prev_content.
* Default: true. <strong>This property is experimental and may change.</strong>
*/
module.exports.MatrixEvent = function MatrixEvent(
event,
) {
this.event = event || {};
this.sender = null;
this.target = null;
this.status = null;
this.error = null;
this.forwardLooking = true;
this._pushActions = null;
this._date = this.event.origin_server_ts ?
new Date(this.event.origin_server_ts) : null;
this._clearEvent = {};
this._keysProved = {};
this._keysClaimed = {};
};
utils.inherits(module.exports.MatrixEvent, EventEmitter);
utils.extend(module.exports.MatrixEvent.prototype, {
/**
* Get the event_id for this event.
* @return {string} The event ID, e.g. <code>$143350589368169JsLZx:localhost
* </code>
*/
getId: function() {
return this.event.event_id;
},
/**
* Get the user_id for this event.
* @return {string} The user ID, e.g. <code>@alice:matrix.org</code>
*/
getSender: function() {
return this.event.sender || this.event.user_id; // v2 / v1
},
/**
* Get the (decrypted, if necessary) type of event.
*
* @return {string} The event type, e.g. <code>m.room.message</code>
*/
getType: function() {
return this._clearEvent.type || this.event.type;
},
/**
* Get the (possibly encrypted) type of the event that will be sent to the
* homeserver.
*
* @return {string} The event type.
*/
getWireType: function() {
return this.event.type;
},
/**
* Get the room_id for this event. This will return <code>undefined</code>
* for <code>m.presence</code> events.
* @return {string} The room ID, e.g. <code>!cURbafjkfsMDVwdRDQ:matrix.org
* </code>
*/
getRoomId: function() {
return this.event.room_id;
},
/**
* Get the timestamp of this event.
* @return {Number} The event timestamp, e.g. <code>1433502692297</code>
*/
getTs: function() {
return this.event.origin_server_ts;
},
/**
* Get the timestamp of this event, as a Date object.
* @return {Date} The event date, e.g. <code>new Date(1433502692297)</code>
*/
getDate: function() {
return this._date;
},
/**
* Get the (decrypted, if necessary) event content JSON.
*
* @return {Object} The event content JSON, or an empty object.
*/
getContent: function() {
return this._clearEvent.content || this.event.content || {};
},
/**
* Get the (possibly encrypted) event content JSON that will be sent to the
* homeserver.
*
* @return {Object} The event content JSON, or an empty object.
*/
getWireContent: function() {
return this.event.content || {};
},
/**
* Get the previous event content JSON. This will only return something for
* state events which exist in the timeline.
* @return {Object} The previous event content JSON, or an empty object.
*/
getPrevContent: function() {
// v2 then v1 then default
return this.getUnsigned().prev_content || this.event.prev_content || {};
},
/**
* Get either 'content' or 'prev_content' depending on if this event is
* 'forward-looking' or not. This can be modified via event.forwardLooking.
* In practice, this means we get the chronologically earlier content value
* for this event (this method should surely be called getEarlierContent)
* <strong>This method is experimental and may change.</strong>
* @return {Object} event.content if this event is forward-looking, else
* event.prev_content.
*/
getDirectionalContent: function() {
return this.forwardLooking ? this.getContent() : this.getPrevContent();
},
/**
* Get the age of this event. This represents the age of the event when the
* event arrived at the device, and not the age of the event when this
* function was called.
* @return {Number} The age of this event in milliseconds.
*/
getAge: function() {
return this.getUnsigned().age || this.event.age; // v2 / v1
},
/**
* Get the event state_key if it has one. This will return <code>undefined
* </code> for message events.
* @return {string} The event's <code>state_key</code>.
*/
getStateKey: function() {
return this.event.state_key;
},
/**
* Check if this event is a state event.
* @return {boolean} True if this is a state event.
*/
isState: function() {
return this.event.state_key !== undefined;
},
/**
* Replace the content of this event with encrypted versions.
* (This is used when sending an event; it should not be used by applications).
*
* @internal
*
* @param {string} crypto_type type of the encrypted event - typically
* <tt>"m.room.encrypted"</tt>
*
* @param {object} crypto_content raw 'content' for the encrypted event.
* @param {object} keys The local keys claimed and proved by this event.
*/
makeEncrypted: function(crypto_type, crypto_content, keys) {
// keep the plain-text data for 'view source'
this._clearEvent = {
type: this.event.type,
content: this.event.content,
};
this.event.type = crypto_type;
this.event.content = crypto_content;
this._keysProved = keys;
this._keysClaimed = keys;
},
/**
* Update the cleartext data on this event.
*
* (This is used after decrypting an event; it should not be used by applications).
*
* @internal
*
* @fires module:models/event.MatrixEvent#"Event.decrypted"
*
* @param {Object} clearEvent The plaintext payload for the event
* (typically containing <tt>type</tt> and <tt>content</tt> fields).
*
* @param {Object=} keysProved Keys owned by the sender of this event.
* See {@link module:models/event.MatrixEvent#getKeysProved}.
*
* @param {Object=} keysClaimed Keys the sender of this event claims.
* See {@link module:models/event.MatrixEvent#getKeysClaimed}.
*/
setClearData: function(clearEvent, keysProved, keysClaimed) {
this._clearEvent = clearEvent;
this._keysProved = keysProved || {};
this._keysClaimed = keysClaimed || {};
this.emit("Event.decrypted", this);
},
/**
* Check if the event is encrypted.
* @return {boolean} True if this event is encrypted.
*/
isEncrypted: function() {
return this.event.type === "m.room.encrypted";
},
/**
* The curve25519 key that sent this event
* @return {string}
*/
getSenderKey: function() {
return this.getKeysProved().curve25519 || null;
},
/**
* The keys that must have been owned by the sender of this encrypted event.
* <p>
* These don't necessarily have to come from this event itself, but may be
* implied by the cryptographic session.
*
* @return {Object<string, string>}
*/
getKeysProved: function() {
return this._keysProved;
},
/**
* The additional keys the sender of this encrypted event claims to possess.
* <p>
* These don't necessarily have to come from this event itself, but may be
* implied by the cryptographic session.
* For example megolm messages don't claim keys directly, but instead
* inherit a claim from the olm message that established the session.
*
* @return {Object<string, string>}
*/
getKeysClaimed: function() {
return this._keysClaimed;
},
getUnsigned: function() {
return this.event.unsigned || {};
},
/**
* Update the content of an event in the same way it would be by the server
* if it were redacted before it was sent to us
*
* @param {module:models/event.MatrixEvent} redaction_event
* event causing the redaction
*/
makeRedacted: function(redaction_event) {
// quick sanity-check
if (!redaction_event.event) {
throw new Error("invalid redaction_event in makeRedacted");
}
// we attempt to replicate what we would see from the server if
// the event had been redacted before we saw it.
//
// The server removes (most of) the content of the event, and adds a
// "redacted_because" key to the unsigned section containing the
// redacted event.
if (!this.event.unsigned) {
this.event.unsigned = {};
}
this.event.unsigned.redacted_because = redaction_event.event;
let key;
for (key in this.event) {
if (!this.event.hasOwnProperty(key)) {
continue;
}
if (!_REDACT_KEEP_KEY_MAP[key]) {
delete this.event[key];
}
}
const keeps = _REDACT_KEEP_CONTENT_MAP[this.getType()] || {};
const content = this.getContent();
for (key in content) {
if (!content.hasOwnProperty(key)) {
continue;
}
if (!keeps[key]) {
delete content[key];
}
}
},
/**
* Check if this event has been redacted
*
* @return {boolean} True if this event has been redacted
*/
isRedacted: function() {
return Boolean(this.getUnsigned().redacted_because);
},
/**
* Get the push actions, if known, for this event
*
* @return {?Object} push actions
*/
getPushActions: function() {
return this._pushActions;
},
/**
* Set the push actions for this event.
*
* @param {Object} pushActions push actions
*/
setPushActions: function(pushActions) {
this._pushActions = pushActions;
},
/**
* Replace the `event` property and recalculate any properties based on it.
* @param {Object} event the object to assign to the `event` property
*/
handleRemoteEcho: function(event) {
this.event = event;
// successfully sent.
this.status = null;
this._date = new Date(this.event.origin_server_ts);
},
});
/* http://matrix.org/docs/spec/r0.0.1/client_server.html#redactions says:
*
* the server should strip off any keys not in the following list:
* event_id
* type
* room_id
* user_id
* state_key
* prev_state
* content
* [we keep 'unsigned' as well, since that is created by the local server]
*
* The content object should also be stripped of all keys, unless it is one of
* one of the following event types:
* m.room.member allows key membership
* m.room.create allows key creator
* m.room.join_rules allows key join_rule
* m.room.power_levels allows keys ban, events, events_default, kick,
* redact, state_default, users, users_default.
* m.room.aliases allows key aliases
*/
// a map giving the keys we keep when an event is redacted
const _REDACT_KEEP_KEY_MAP = [
'event_id', 'type', 'room_id', 'user_id', 'state_key', 'prev_state',
'content', 'unsigned',
].reduce(function(ret, val) {
ret[val] = 1; return ret;
}, {});
// a map from event type to the .content keys we keep when an event is redacted
const _REDACT_KEEP_CONTENT_MAP = {
'm.room.member': {'membership': 1},
'm.room.create': {'creator': 1},
'm.room.join_rules': {'join_rule': 1},
'm.room.power_levels': {'ban': 1, 'events': 1, 'events_default': 1,
'kick': 1, 'redact': 1, 'state_default': 1,
'users': 1, 'users_default': 1,
},
'm.room.aliases': {'aliases': 1},
};
/**
* Fires when an event is decrypted
*
* @event module:models/event.MatrixEvent#"Event.decrypted"
*
* @param {module:models/event.MatrixEvent} event
* The matrix event which has been decrypted
*/
@@ -1,14 +1,33 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/room-member
*/
var EventEmitter = require("events").EventEmitter;
const EventEmitter = require("events").EventEmitter;
const ContentRepo = require("../content-repo");
var utils = require("../utils");
const utils = require("../utils");
/**
* Construct a new room member.
*
* @constructor
* @alias module:models/room-member
*
* @param {string} roomId The room ID of the member.
* @param {string} userId The user ID of the member.
* @prop {string} roomId The room ID for this member.
@@ -33,7 +52,7 @@ function RoomMember(roomId, userId) {
this.user = null;
this.membership = null;
this.events = {
member: null
member: null,
};
this._updateModifiedTime();
}
@@ -54,18 +73,18 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) {
}
this.events.member = event;
var oldMembership = this.membership;
const oldMembership = this.membership;
this.membership = event.getDirectionalContent().membership;
var oldName = this.name;
const oldName = this.name;
this.name = calculateDisplayName(this, event, roomState);
if (oldMembership !== this.membership) {
this._updateModifiedTime();
this.emit("RoomMember.membership", event, this);
this.emit("RoomMember.membership", event, this, oldMembership);
}
if (oldName !== this.name) {
this._updateModifiedTime();
this.emit("RoomMember.name", event, this);
this.emit("RoomMember.name", event, this, oldName);
}
};
@@ -80,17 +99,20 @@ RoomMember.prototype.setPowerLevelEvent = function(powerLevelEvent) {
if (powerLevelEvent.getType() !== "m.room.power_levels") {
return;
}
var maxLevel = powerLevelEvent.getContent().users_default || 0;
let maxLevel = powerLevelEvent.getContent().users_default || 0;
utils.forEach(utils.values(powerLevelEvent.getContent().users), function(lvl) {
maxLevel = Math.max(maxLevel, lvl);
});
var oldPowerLevel = this.powerLevel;
var oldPowerLevelNorm = this.powerLevelNorm;
this.powerLevel = (
powerLevelEvent.getContent().users[this.userId] ||
powerLevelEvent.getContent().users_default ||
0
);
const oldPowerLevel = this.powerLevel;
const oldPowerLevelNorm = this.powerLevelNorm;
if (powerLevelEvent.getContent().users[this.userId] !== undefined) {
this.powerLevel = powerLevelEvent.getContent().users[this.userId];
} else if (powerLevelEvent.getContent().users_default !== undefined) {
this.powerLevel = powerLevelEvent.getContent().users_default;
} else {
this.powerLevel = 0;
}
this.powerLevelNorm = 0;
if (maxLevel > 0) {
this.powerLevelNorm = (this.powerLevel * 100) / maxLevel;
@@ -114,9 +136,9 @@ RoomMember.prototype.setTypingEvent = function(event) {
if (event.getType() !== "m.typing") {
return;
}
var oldTyping = this.typing;
const oldTyping = this.typing;
this.typing = false;
var typingList = event.getContent().user_ids;
const typingList = event.getContent().user_ids;
if (!utils.isArray(typingList)) {
// malformed event :/ bail early. TODO: whine?
return;
@@ -147,24 +169,49 @@ RoomMember.prototype.getLastModifiedTime = function() {
return this._modified;
};
function calculateDisplayName(member, event, roomState) {
var displayName = event.getDirectionalContent().displayname;
var selfUserId = member.userId;
/*
// FIXME: this would be great but still needs to use the
// full userId to disambiguate if needed...
if (!displayName) {
var matches = selfUserId.match(/^@(.*?):/);
if (matches) {
return matches[1];
}
else {
return selfUserId;
}
/**
* Get the avatar URL for a room member.
* @param {string} baseUrl The base homeserver URL See
* {@link module:client~MatrixClient#getHomeserverUrl}.
* @param {Number} width The desired width of the thumbnail.
* @param {Number} height The desired height of the thumbnail.
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @param {Boolean} allowDefault (optional) Passing false causes this method to
* return null if the user has no avatar image. Otherwise, a default image URL
* will be returned. Default: true.
* @param {Boolean} allowDirectLinks (optional) If true, the avatar URL will be
* returned even if it is a direct hyperlink rather than a matrix content URL.
* If false, any non-matrix content URLs will be ignored. Setting this option to
* true will expose URLs that, if fetched, will leak information about the user
* to anyone who they share a room with.
* @return {?string} the avatar URL or null.
*/
RoomMember.prototype.getAvatarUrl =
function(baseUrl, width, height, resizeMethod, allowDefault, allowDirectLinks) {
if (allowDefault === undefined) {
allowDefault = true;
}
*/
if (!this.events.member && !allowDefault) {
return null;
}
const rawUrl = this.events.member ? this.events.member.getContent().avatar_url : null;
const httpUrl = ContentRepo.getHttpUriForMxc(
baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks,
);
if (httpUrl) {
return httpUrl;
} else if (allowDefault) {
return ContentRepo.getIdenticonUri(
baseUrl, this.userId, width, height,
);
}
return null;
};
function calculateDisplayName(member, event, roomState) {
const displayName = event.getDirectionalContent().displayname;
const selfUserId = member.userId;
if (!displayName) {
return selfUserId;
@@ -174,18 +221,13 @@ function calculateDisplayName(member, event, roomState) {
return displayName;
}
var stateEvents = utils.filter(
roomState.getStateEvents("m.room.member"),
function(e) {
return e.getContent().displayname === displayName &&
e.getSender() !== selfUserId;
}
);
if (stateEvents.length > 0) {
// need to disambiguate
const userIds = roomState.getUserIdsWithDisplayName(displayName);
const otherUsers = userIds.filter(function(u) {
return u !== selfUserId;
});
if (otherUsers.length > 0) {
return displayName + " (" + selfUserId + ")";
}
return displayName;
}
@@ -199,6 +241,8 @@ module.exports = RoomMember;
* @event module:client~MatrixClient#"RoomMember.name"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomMember} member The member whose RoomMember.name changed.
* @param {string?} oldName The previous name. Null if the member didn't have a
* name previously.
* @example
* matrixClient.on("RoomMember.name", function(event, member){
* var newName = member.name;
@@ -210,8 +254,10 @@ module.exports = RoomMember;
* @event module:client~MatrixClient#"RoomMember.membership"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomMember} member The member whose RoomMember.membership changed.
* @param {string?} oldMembership The previous membership state. Null if it's a
* new member.
* @example
* matrixClient.on("RoomMember.membership", function(event, member){
* matrixClient.on("RoomMember.membership", function(event, member, oldMembership){
* var newState = member.membership;
* });
*/
+434
View File
@@ -0,0 +1,434 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/room-state
*/
const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
const RoomMember = require("./room-member");
/**
* Construct room state.
* @constructor
* @param {?string} roomId Optional. The ID of the room which has this state.
* If none is specified it just tracks paginationTokens, useful for notifTimelineSet
* @prop {Object.<string, RoomMember>} members The room member dictionary, keyed
* on the user's ID.
* @prop {Object.<string, Object.<string, MatrixEvent>>} events The state
* events dictionary, keyed on the event type and then the state_key value.
* @prop {string} paginationToken The pagination token for this state.
*/
function RoomState(roomId) {
this.roomId = roomId;
this.members = {
// userId: RoomMember
};
this.events = {
// eventType: { stateKey: MatrixEvent }
};
this.paginationToken = null;
this._sentinels = {
// userId: RoomMember
};
this._updateModifiedTime();
this._displayNameToUserIds = {};
this._userIdsToDisplayNames = {};
this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite
}
utils.inherits(RoomState, EventEmitter);
/**
* Get all RoomMembers in this room.
* @return {Array<RoomMember>} A list of RoomMembers.
*/
RoomState.prototype.getMembers = function() {
return utils.values(this.members);
};
/**
* Get a room member by their user ID.
* @param {string} userId The room member's user ID.
* @return {RoomMember} The member or null if they do not exist.
*/
RoomState.prototype.getMember = function(userId) {
return this.members[userId] || null;
};
/**
* Get a room member whose properties will not change with this room state. You
* typically want this if you want to attach a RoomMember to a MatrixEvent which
* may no longer be represented correctly by Room.currentState or Room.oldState.
* The term 'sentinel' refers to the fact that this RoomMember is an unchanging
* guardian for state at this particular point in time.
* @param {string} userId The room member's user ID.
* @return {RoomMember} The member or null if they do not exist.
*/
RoomState.prototype.getSentinelMember = function(userId) {
return this._sentinels[userId] || null;
};
/**
* Get state events from the state of the room.
* @param {string} eventType The event type of the state event.
* @param {string} stateKey Optional. The state_key of the state event. If
* this is <code>undefined</code> then all matching state events will be
* returned.
* @return {MatrixEvent[]|MatrixEvent} A list of events if state_key was
* <code>undefined</code>, else a single event (or null if no match found).
*/
RoomState.prototype.getStateEvents = function(eventType, stateKey) {
if (!this.events[eventType]) {
// no match
return stateKey === undefined ? [] : null;
}
if (stateKey === undefined) { // return all values
return utils.values(this.events[eventType]);
}
const event = this.events[eventType][stateKey];
return event ? event : null;
};
/**
* Add an array of one or more state MatrixEvents, overwriting
* any existing state with the same {type, stateKey} tuple. Will fire
* "RoomState.events" for every event added. May fire "RoomState.members"
* if there are <code>m.room.member</code> events.
* @param {MatrixEvent[]} stateEvents a list of state events for this room.
* @fires module:client~MatrixClient#event:"RoomState.members"
* @fires module:client~MatrixClient#event:"RoomState.newMember"
* @fires module:client~MatrixClient#event:"RoomState.events"
*/
RoomState.prototype.setStateEvents = function(stateEvents) {
const self = this;
this._updateModifiedTime();
// update the core event dict
utils.forEach(stateEvents, function(event) {
if (event.getRoomId() !== self.roomId) {
return;
}
if (!event.isState()) {
return;
}
if (self.events[event.getType()] === undefined) {
self.events[event.getType()] = {};
}
self.events[event.getType()][event.getStateKey()] = event;
if (event.getType() === "m.room.member") {
_updateDisplayNameCache(
self, event.getStateKey(), event.getContent().displayname,
);
_updateThirdPartyTokenCache(self, event);
}
self.emit("RoomState.events", event, self);
});
// update higher level data structures. This needs to be done AFTER the
// core event dict as these structures may depend on other state events in
// the given array (e.g. disambiguating display names in one go to do both
// clashing names rather than progressively which only catches 1 of them).
utils.forEach(stateEvents, function(event) {
if (event.getRoomId() !== self.roomId) {
return;
}
if (!event.isState()) {
return;
}
if (event.getType() === "m.room.member") {
const userId = event.getStateKey();
// leave events apparently elide the displayname or avatar_url,
// so let's fake one up so that we don't leak user ids
// into the timeline
if (event.getContent().membership === "leave" ||
event.getContent().membership === "ban") {
event.getContent().avatar_url =
event.getContent().avatar_url ||
event.getPrevContent().avatar_url;
event.getContent().displayname =
event.getContent().displayname ||
event.getPrevContent().displayname;
}
let member = self.members[userId];
if (!member) {
member = new RoomMember(event.getRoomId(), userId);
self.emit("RoomState.newMember", event, self, member);
}
// Add a new sentinel for this change. We apply the same
// operations to both sentinel and member rather than deep copying
// so we don't make assumptions about the properties of RoomMember
// (e.g. and manage to break it because deep copying doesn't do
// everything).
const sentinel = new RoomMember(event.getRoomId(), userId);
utils.forEach([member, sentinel], function(roomMember) {
roomMember.setMembershipEvent(event, self);
// this member may have a power level already, so set it.
const pwrLvlEvent = self.getStateEvents("m.room.power_levels", "");
if (pwrLvlEvent) {
roomMember.setPowerLevelEvent(pwrLvlEvent);
}
});
self._sentinels[userId] = sentinel;
self.members[userId] = member;
self.emit("RoomState.members", event, self, member);
} else if (event.getType() === "m.room.power_levels") {
const members = utils.values(self.members);
utils.forEach(members, function(member) {
member.setPowerLevelEvent(event);
self.emit("RoomState.members", event, self, member);
});
}
});
};
/**
* Set the current typing event for this room.
* @param {MatrixEvent} event The typing event
*/
RoomState.prototype.setTypingEvent = function(event) {
utils.forEach(utils.values(this.members), function(member) {
member.setTypingEvent(event);
});
};
/**
* Get the m.room.member event which has the given third party invite token.
*
* @param {string} token The token
* @return {?MatrixEvent} The m.room.member event or null
*/
RoomState.prototype.getInviteForThreePidToken = function(token) {
return this._tokenToInvite[token] || null;
};
/**
* Update the last modified time to the current time.
*/
RoomState.prototype._updateModifiedTime = function() {
this._modified = Date.now();
};
/**
* Get the timestamp when this room state was last updated. This timestamp is
* updated when this object has received new state events.
* @return {number} The timestamp
*/
RoomState.prototype.getLastModifiedTime = function() {
return this._modified;
};
/**
* Get user IDs with the specified display name.
* @param {string} displayName The display name to get user IDs from.
* @return {string[]} An array of user IDs or an empty array.
*/
RoomState.prototype.getUserIdsWithDisplayName = function(displayName) {
return this._displayNameToUserIds[displayName] || [];
};
/**
* Short-form for maySendEvent('m.room.message', userId)
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* message events into the given room.
*/
RoomState.prototype.maySendMessage = function(userId) {
return this._maySendEventOfType('m.room.message', userId, false);
};
/**
* Returns true if the given user ID has permission to send a normal
* event of type `eventType` into this room.
* @param {string} eventType The type of event to test
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* the given type of event into this room,
* according to the room's state.
*/
RoomState.prototype.maySendEvent = function(eventType, userId) {
return this._maySendEventOfType(eventType, userId, false);
};
/**
* Returns true if the given MatrixClient has permission to send a state
* event of type `stateEventType` into this room.
* @param {string} stateEventType The type of state events to test
* @param {MatrixClient} cli The client to test permission for
* @return {boolean} true if the given client should be permitted to send
* the given type of state event into this room,
* according to the room's state.
*/
RoomState.prototype.mayClientSendStateEvent = function(stateEventType, cli) {
if (cli.isGuest()) {
return false;
}
return this.maySendStateEvent(stateEventType, cli.credentials.userId);
};
/**
* Returns true if the given user ID has permission to send a state
* event of type `stateEventType` into this room.
* @param {string} stateEventType The type of state events to test
* @param {string} userId The user ID of the user to test permission for
* @return {boolean} true if the given user ID should be permitted to send
* the given type of state event into this room,
* according to the room's state.
*/
RoomState.prototype.maySendStateEvent = function(stateEventType, userId) {
return this._maySendEventOfType(stateEventType, userId, true);
};
/**
* Returns true if the given user ID has permission to send a normal or state
* event of type `eventType` into this room.
* @param {string} eventType The type of event to test
* @param {string} userId The user ID of the user to test permission for
* @param {boolean} state If true, tests if the user may send a state
event of this type. Otherwise tests whether
they may send a regular event.
* @return {boolean} true if the given user ID should be permitted to send
* the given type of event into this room,
* according to the room's state.
*/
RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
const member = this.getMember(userId);
if (!member || member.membership == 'leave') {
return false;
}
const power_levels_event = this.getStateEvents('m.room.power_levels', '');
let power_levels;
let events_levels = {};
let state_default = 0;
let events_default = 0;
if (power_levels_event) {
power_levels = power_levels_event.getContent();
events_levels = power_levels.events || {};
if (power_levels.state_default !== undefined) {
state_default = power_levels.state_default;
} else {
state_default = 50;
}
if (power_levels.events_default !== undefined) {
events_default = power_levels.events_default;
}
}
let required_level = state ? state_default : events_default;
if (events_levels[eventType] !== undefined) {
required_level = events_levels[eventType];
}
return member.powerLevel >= required_level;
};
/**
* The RoomState class.
*/
module.exports = RoomState;
function _updateThirdPartyTokenCache(roomState, memberEvent) {
if (!memberEvent.getContent().third_party_invite) {
return;
}
const token = (memberEvent.getContent().third_party_invite.signed || {}).token;
if (!token) {
return;
}
const threePidInvite = roomState.getStateEvents(
"m.room.third_party_invite", token,
);
if (!threePidInvite) {
return;
}
roomState._tokenToInvite[token] = memberEvent;
}
function _updateDisplayNameCache(roomState, userId, displayName) {
const oldName = roomState._userIdsToDisplayNames[userId];
delete roomState._userIdsToDisplayNames[userId];
if (oldName) {
// Remove the old name from the cache.
// We clobber the user_id > name lookup but the name -> [user_id] lookup
// means we need to remove that user ID from that array rather than nuking
// the lot.
const existingUserIds = roomState._displayNameToUserIds[oldName] || [];
for (let i = 0; i < existingUserIds.length; i++) {
if (existingUserIds[i] === userId) {
// remove this user ID from this array
existingUserIds.splice(i, 1);
i--;
}
}
roomState._displayNameToUserIds[oldName] = existingUserIds;
}
roomState._userIdsToDisplayNames[userId] = displayName;
if (!roomState._displayNameToUserIds[displayName]) {
roomState._displayNameToUserIds[displayName] = [];
}
roomState._displayNameToUserIds[displayName].push(userId);
}
/**
* Fires whenever the event dictionary in room state is updated.
* @event module:client~MatrixClient#"RoomState.events"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomState} state The room state whose RoomState.events dictionary
* was updated.
* @example
* matrixClient.on("RoomState.events", function(event, state){
* var newStateEvent = event;
* });
*/
/**
* Fires whenever a member in the members dictionary is updated in any way.
* @event module:client~MatrixClient#"RoomState.members"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomState} state The room state whose RoomState.members dictionary
* was updated.
* @param {RoomMember} member The room member that was updated.
* @example
* matrixClient.on("RoomState.members", function(event, state, member){
* var newMembershipState = member.membership;
* });
*/
/**
* Fires whenever a member is added to the members dictionary. The RoomMember
* will not be fully populated yet (e.g. no membership state).
* @event module:client~MatrixClient#"RoomState.newMember"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {RoomState} state The room state whose RoomState.members dictionary
* was updated with a new entry.
* @param {RoomMember} member The room member that was added.
* @example
* matrixClient.on("RoomState.newMember", function(event, state, member){
* // add event listeners on 'member'
* });
*/
@@ -1,3 +1,18 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/room-summary
+1365
View File
File diff suppressed because it is too large Load Diff
+66
View File
@@ -0,0 +1,66 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/search-result
*/
const EventContext = require("./event-context");
const utils = require("../utils");
/**
* Construct a new SearchResult
*
* @param {number} rank where this SearchResult ranks in the results
* @param {event-context.EventContext} eventContext the matching event and its
* context
*
* @constructor
*/
function SearchResult(rank, eventContext) {
this.rank = rank;
this.context = eventContext;
}
/**
* Create a SearchResponse from the response to /search
* @static
* @param {Object} jsonObj
* @param {function} eventMapper
* @return {SearchResult}
*/
SearchResult.fromJson = function(jsonObj, eventMapper) {
const jsonContext = jsonObj.context || {};
const events_before = jsonContext.events_before || [];
const events_after = jsonContext.events_after || [];
const context = new EventContext(eventMapper(jsonObj.result));
context.setPaginateToken(jsonContext.start, true);
context.addEvents(utils.map(events_before, eventMapper), true);
context.addEvents(utils.map(events_after, eventMapper), false);
context.setPaginateToken(jsonContext.end, false);
return new SearchResult(jsonObj.rank, context);
};
/**
* The SearchResult class
*/
module.exports = SearchResult;
+241
View File
@@ -0,0 +1,241 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module models/user
*/
const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
/**
* Construct a new User. A User must have an ID and can optionally have extra
* information associated with it.
* @constructor
* @param {string} userId Required. The ID of this user.
* @prop {string} userId The ID of the user.
* @prop {Object} info The info object supplied in the constructor.
* @prop {string} displayName The 'displayname' of the user if known.
* @prop {string} avatarUrl The 'avatar_url' of the user if known.
* @prop {string} presence The presence enum if known.
* @prop {string} presenceStatusMsg The presence status message if known.
* @prop {Number} lastActiveAgo The time elapsed in ms since the user interacted
* proactively with the server, or we saw a message from the user
* @prop {Number} lastPresenceTs Timestamp (ms since the epoch) for when we last
* received presence data for this user. We can subtract
* lastActiveAgo from this to approximate an absolute value for
* when a user was last active.
* @prop {Boolean} currentlyActive Whether we should consider lastActiveAgo to be
* an approximation and that the user should be seen as active 'now'
* @prop {Object} events The events describing this user.
* @prop {MatrixEvent} events.presence The m.presence event for this user.
*/
function User(userId) {
this.userId = userId;
this.presence = "offline";
this.presenceStatusMsg = null;
this.displayName = userId;
this.rawDisplayName = userId;
this.avatarUrl = null;
this.lastActiveAgo = 0;
this.lastPresenceTs = 0;
this.currentlyActive = false;
this.events = {
presence: null,
profile: null,
};
this._updateModifiedTime();
}
utils.inherits(User, EventEmitter);
/**
* Update this User with the given presence event. May fire "User.presence",
* "User.avatarUrl" and/or "User.displayName" if this event updates this user's
* properties.
* @param {MatrixEvent} event The <code>m.presence</code> event.
* @fires module:client~MatrixClient#event:"User.presence"
* @fires module:client~MatrixClient#event:"User.displayName"
* @fires module:client~MatrixClient#event:"User.avatarUrl"
*/
User.prototype.setPresenceEvent = function(event) {
if (event.getType() !== "m.presence") {
return;
}
const firstFire = this.events.presence === null;
this.events.presence = event;
const eventsToFire = [];
if (event.getContent().presence !== this.presence || firstFire) {
eventsToFire.push("User.presence");
}
if (event.getContent().avatar_url &&
event.getContent().avatar_url !== this.avatarUrl) {
eventsToFire.push("User.avatarUrl");
}
if (event.getContent().displayname &&
event.getContent().displayname !== this.displayName) {
eventsToFire.push("User.displayName");
}
if (event.getContent().currently_active !== undefined &&
event.getContent().currently_active !== this.currentlyActive) {
eventsToFire.push("User.currentlyActive");
}
this.presence = event.getContent().presence;
eventsToFire.push("User.lastPresenceTs");
if (event.getContent().status_msg) {
this.presenceStatusMsg = event.getContent().status_msg;
}
if (event.getContent().displayname) {
this.displayName = event.getContent().displayname;
}
if (event.getContent().avatar_url) {
this.avatarUrl = event.getContent().avatar_url;
}
this.lastActiveAgo = event.getContent().last_active_ago;
this.lastPresenceTs = Date.now();
this.currentlyActive = event.getContent().currently_active;
this._updateModifiedTime();
for (let i = 0; i < eventsToFire.length; i++) {
this.emit(eventsToFire[i], event, this);
}
};
/**
* Manually set this user's display name. No event is emitted in response to this
* as there is no underlying MatrixEvent to emit with.
* @param {string} name The new display name.
*/
User.prototype.setDisplayName = function(name) {
const oldName = this.displayName;
this.displayName = name;
if (name !== oldName) {
this._updateModifiedTime();
}
};
/**
* Manually set this user's non-disambiguated display name. No event is emitted
* in response to this as there is no underlying MatrixEvent to emit with.
* @param {string} name The new display name.
*/
User.prototype.setRawDisplayName = function(name) {
this.rawDisplayName = name;
};
/**
* Manually set this user's avatar URL. No event is emitted in response to this
* as there is no underlying MatrixEvent to emit with.
* @param {string} url The new avatar URL.
*/
User.prototype.setAvatarUrl = function(url) {
const oldUrl = this.avatarUrl;
this.avatarUrl = url;
if (url !== oldUrl) {
this._updateModifiedTime();
}
};
/**
* Update the last modified time to the current time.
*/
User.prototype._updateModifiedTime = function() {
this._modified = Date.now();
};
/**
* Get the timestamp when this User was last updated. This timestamp is
* updated when this User receives a new Presence event which has updated a
* property on this object. It is updated <i>before</i> firing events.
* @return {number} The timestamp
*/
User.prototype.getLastModifiedTime = function() {
return this._modified;
};
/**
* Get the absolute timestamp when this User was last known active on the server.
* It is *NOT* accurate if this.currentlyActive is true.
* @return {number} The timestamp
*/
User.prototype.getLastActiveTs = function() {
return this.lastPresenceTs - this.lastActiveAgo;
};
/**
* The User class.
*/
module.exports = User;
/**
* Fires whenever any user's lastPresenceTs changes,
* ie. whenever any presence event is received for a user.
* @event module:client~MatrixClient#"User.lastPresenceTs"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {User} user The user whose User.lastPresenceTs changed.
* @example
* matrixClient.on("User.lastPresenceTs", function(event, user){
* var newlastPresenceTs = user.lastPresenceTs;
* });
*/
/**
* Fires whenever any user's presence changes.
* @event module:client~MatrixClient#"User.presence"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {User} user The user whose User.presence changed.
* @example
* matrixClient.on("User.presence", function(event, user){
* var newPresence = user.presence;
* });
*/
/**
* Fires whenever any user's currentlyActive changes.
* @event module:client~MatrixClient#"User.currentlyActive"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {User} user The user whose User.currentlyActive changed.
* @example
* matrixClient.on("User.currentlyActive", function(event, user){
* var newCurrentlyActive = user.currentlyActive;
* });
*/
/**
* Fires whenever any user's display name changes.
* @event module:client~MatrixClient#"User.displayName"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {User} user The user whose User.displayName changed.
* @example
* matrixClient.on("User.displayName", function(event, user){
* var newName = user.displayName;
* });
*/
/**
* Fires whenever any user's avatar URL changes.
* @event module:client~MatrixClient#"User.avatarUrl"
* @param {MatrixEvent} event The matrix event which caused this event to fire.
* @param {User} user The user whose User.avatarUrl changed.
* @example
* matrixClient.on("User.avatarUrl", function(event, user){
* var newUrl = user.avatarUrl;
* });
*/
+343
View File
@@ -0,0 +1,343 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module pushprocessor
*/
/**
* Construct a Push Processor.
* @constructor
* @param {Object} client The Matrix client object to use
*/
function PushProcessor(client) {
const escapeRegExp = function(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
const matchingRuleFromKindSet = function(ev, kindset, device) {
const rulekinds_in_order = ['override', 'content', 'room', 'sender', 'underride'];
for (let ruleKindIndex = 0;
ruleKindIndex < rulekinds_in_order.length;
++ruleKindIndex) {
const kind = rulekinds_in_order[ruleKindIndex];
const ruleset = kindset[kind];
for (let ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) {
const rule = ruleset[ruleIndex];
if (!rule.enabled) {
continue;
}
const rawrule = templateRuleToRaw(kind, rule, device);
if (!rawrule) {
continue;
}
if (ruleMatchesEvent(rawrule, ev)) {
rule.kind = kind;
return rule;
}
}
}
return null;
};
const templateRuleToRaw = function(kind, tprule, device) {
const rawrule = {
'rule_id': tprule.rule_id,
'actions': tprule.actions,
'conditions': [],
};
switch (kind) {
case 'underride':
case 'override':
rawrule.conditions = tprule.conditions;
break;
case 'room':
if (!tprule.rule_id) {
return null;
}
rawrule.conditions.push({
'kind': 'event_match',
'key': 'room_id',
'pattern': tprule.rule_id,
});
break;
case 'sender':
if (!tprule.rule_id) {
return null;
}
rawrule.conditions.push({
'kind': 'event_match',
'key': 'user_id',
'pattern': tprule.rule_id,
});
break;
case 'content':
if (!tprule.pattern) {
return null;
}
rawrule.conditions.push({
'kind': 'event_match',
'key': 'content.body',
'pattern': tprule.pattern,
});
break;
}
if (device) {
rawrule.conditions.push({
'kind': 'device',
'profile_tag': device,
});
}
return rawrule;
};
const ruleMatchesEvent = function(rule, ev) {
let ret = true;
for (let i = 0; i < rule.conditions.length; ++i) {
const cond = rule.conditions[i];
ret &= eventFulfillsCondition(cond, ev);
}
//console.log("Rule "+rule.rule_id+(ret ? " matches" : " doesn't match"));
return ret;
};
const eventFulfillsCondition = function(cond, ev) {
const condition_functions = {
"event_match": eventFulfillsEventMatchCondition,
"device": eventFulfillsDeviceCondition,
"contains_display_name": eventFulfillsDisplayNameCondition,
"room_member_count": eventFulfillsRoomMemberCountCondition,
};
if (condition_functions[cond.kind]) {
return condition_functions[cond.kind](cond, ev);
}
return true;
};
const eventFulfillsRoomMemberCountCondition = function(cond, ev) {
if (!cond.is) {
return false;
}
const room = client.getRoom(ev.getRoomId());
if (!room || !room.currentState || !room.currentState.members) {
return false;
}
const memberCount = Object.keys(room.currentState.members).filter(function(m) {
return room.currentState.members[m].membership == 'join';
}).length;
const m = cond.is.match(/^([=<>]*)([0-9]*)$/);
if (!m) {
return false;
}
const ineq = m[1];
const rhs = parseInt(m[2]);
if (isNaN(rhs)) {
return false;
}
switch (ineq) {
case '':
case '==':
return memberCount == rhs;
case '<':
return memberCount < rhs;
case '>':
return memberCount > rhs;
case '<=':
return memberCount <= rhs;
case '>=':
return memberCount >= rhs;
default:
return false;
}
};
const eventFulfillsDisplayNameCondition = function(cond, ev) {
const content = ev.getContent();
if (!content || !content.body || typeof content.body != 'string') {
return false;
}
const room = client.getRoom(ev.getRoomId());
if (!room || !room.currentState || !room.currentState.members ||
!room.currentState.getMember(client.credentials.userId)) {
return false;
}
const displayName = room.currentState.getMember(client.credentials.userId).name;
// N.B. we can't use \b as it chokes on unicode. however \W seems to be okay
// as shorthand for [^0-9A-Za-z_].
const pat = new RegExp("(^|\\W)" + escapeRegExp(displayName) + "(\\W|$)", 'i');
return content.body.search(pat) > -1;
};
const eventFulfillsDeviceCondition = function(cond, ev) {
return false; // XXX: Allow a profile tag to be set for the web client instance
};
const eventFulfillsEventMatchCondition = function(cond, ev) {
const val = valueForDottedKey(cond.key, ev);
if (!val || typeof val != 'string') {
return false;
}
let pat;
if (cond.key == 'content.body') {
pat = '(^|\\W)' + globToRegexp(cond.pattern) + '(\\W|$)';
} else {
pat = '^' + globToRegexp(cond.pattern) + '$';
}
const regex = new RegExp(pat, 'i');
return !!val.match(regex);
};
const globToRegexp = function(glob) {
// From
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
// Because micromatch is about 130KB with dependencies,
// and minimatch is not much better.
let pat = escapeRegExp(glob);
pat = pat.replace(/\\\*/, '.*');
pat = pat.replace(/\?/, '.');
pat = pat.replace(/\\\[(!|)(.*)\\]/, function(match, p1, p2, offset, string) {
const first = p1 && '^' || '';
const second = p2.replace(/\\\-/, '-');
return '[' + first + second + ']';
});
return pat;
};
const valueForDottedKey = function(key, ev) {
const parts = key.split('.');
let val;
// special-case the first component to deal with encrypted messages
const firstPart = parts[0];
if (firstPart == 'content') {
val = ev.getContent();
parts.shift();
} else if (firstPart == 'type') {
val = ev.getType();
parts.shift();
} else {
// use the raw event for any other fields
val = ev.event;
}
while (parts.length > 0) {
const thispart = parts.shift();
if (!val[thispart]) {
return null;
}
val = val[thispart];
}
return val;
};
const matchingRuleForEventWithRulesets = function(ev, rulesets) {
if (!rulesets || !rulesets.device) {
return null;
}
if (ev.getSender() == client.credentials.userId) {
return null;
}
const allDevNames = Object.keys(rulesets.device);
for (let i = 0; i < allDevNames.length; ++i) {
const devname = allDevNames[i];
const devrules = rulesets.device[devname];
const matchingRule = matchingRuleFromKindSet(devrules, devname);
if (matchingRule) {
return matchingRule;
}
}
return matchingRuleFromKindSet(ev, rulesets.global);
};
const pushActionsForEventAndRulesets = function(ev, rulesets) {
const rule = matchingRuleForEventWithRulesets(ev, rulesets);
if (!rule) {
return {};
}
const actionObj = PushProcessor.actionListToActionsObject(rule.actions);
// Some actions are implicit in some situations: we add those here
if (actionObj.tweaks.highlight === undefined) {
// if it isn't specified, highlight if it's a content
// rule but otherwise not
actionObj.tweaks.highlight = (rule.kind == 'content');
}
return actionObj;
};
/**
* Get the user's push actions for the given event
*
* @param {module:models/event.MatrixEvent} ev
*
* @return {PushAction}
*/
this.actionsForEvent = function(ev) {
return pushActionsForEventAndRulesets(ev, client.pushRules);
};
}
/**
* Convert a list of actions into a object with the actions as keys and their values
* eg. [ 'notify', { set_tweak: 'sound', value: 'default' } ]
* becomes { notify: true, tweaks: { sound: 'default' } }
* @param {array} actionlist The actions list
*
* @return {object} A object with key 'notify' (true or false) and an object of actions
*/
PushProcessor.actionListToActionsObject = function(actionlist) {
const actionobj = { 'notify': false, 'tweaks': {} };
for (let i = 0; i < actionlist.length; ++i) {
const action = actionlist[i];
if (action === 'notify') {
actionobj.notify = true;
} else if (typeof action === 'object') {
if (action.value === undefined) {
action.value = true;
}
actionobj.tweaks[action.set_tweak] = action.value;
}
}
return actionobj;
};
/**
* @typedef {Object} PushAction
* @type {Object}
* @property {boolean} notify Whether this event should notify the user or not.
* @property {Object} tweaks How this event should be notified.
* @property {boolean} tweaks.highlight Whether this event should be highlighted
* on the UI.
* @property {boolean} tweaks.sound Whether this notification should produce a
* noise.
*/
/** The PushProcessor class. */
module.exports = PushProcessor;
+203
View File
@@ -0,0 +1,203 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/* A re-implementation of the javascript callback functions (setTimeout,
* clearTimeout; setInterval and clearInterval are not yet implemented) which
* try to improve handling of large clock jumps (as seen when
* suspending/resuming the system).
*
* In particular, if a timeout would have fired while the system was suspended,
* it will instead fire as soon as possible after resume.
*/
"use strict";
// we schedule a callback at least this often, to check if we've missed out on
// some wall-clock time due to being suspended.
const TIMER_CHECK_PERIOD_MS = 1000;
// counter, for making up ids to return from setTimeout
let _count = 0;
// the key for our callback with the real global.setTimeout
let _realCallbackKey;
// a sorted list of the callbacks to be run.
// each is an object with keys [runAt, func, params, key].
const _callbackList = [];
// var debuglog = console.log.bind(console);
const debuglog = function() {};
/**
* Replace the function used by this module to get the current time.
*
* Intended for use by the unit tests.
*
* @param {function} f function which should return a millisecond counter
*
* @internal
*/
module.exports.setNow = function(f) {
_now = f || Date.now;
};
let _now = Date.now;
/**
* reimplementation of window.setTimeout, which will call the callback if
* the wallclock time goes past the deadline.
*
* @param {function} func callback to be called after a delay
* @param {Number} delayMs number of milliseconds to delay by
*
* @return {Number} an identifier for this callback, which may be passed into
* clearTimeout later.
*/
module.exports.setTimeout = function(func, delayMs) {
delayMs = delayMs || 0;
if (delayMs < 0) {
delayMs = 0;
}
const params = Array.prototype.slice.call(arguments, 2);
const runAt = _now() + delayMs;
const key = _count++;
debuglog("setTimeout: scheduling cb", key, "at", runAt,
"(delay", delayMs, ")");
const data = {
runAt: runAt,
func: func,
params: params,
key: key,
};
// figure out where it goes in the list
const idx = binarySearch(
_callbackList, function(el) {
return el.runAt - runAt;
},
);
_callbackList.splice(idx, 0, data);
_scheduleRealCallback();
return key;
};
/**
* reimplementation of window.clearTimeout, which mirrors setTimeout
*
* @param {Number} key result from an earlier setTimeout call
*/
module.exports.clearTimeout = function(key) {
if (_callbackList.length === 0) {
return;
}
// remove the element from the list
let i;
for (i = 0; i < _callbackList.length; i++) {
const cb = _callbackList[i];
if (cb.key == key) {
_callbackList.splice(i, 1);
break;
}
}
// iff it was the first one in the list, reschedule our callback.
if (i === 0) {
_scheduleRealCallback();
}
};
// use the real global.setTimeout to schedule a callback to _runCallbacks.
function _scheduleRealCallback() {
if (_realCallbackKey) {
global.clearTimeout(_realCallbackKey);
}
const first = _callbackList[0];
if (!first) {
debuglog("_scheduleRealCallback: no more callbacks, not rescheduling");
return;
}
const now = _now();
const delayMs = Math.min(first.runAt - now, TIMER_CHECK_PERIOD_MS);
debuglog("_scheduleRealCallback: now:", now, "delay:", delayMs);
_realCallbackKey = global.setTimeout(_runCallbacks, delayMs);
}
function _runCallbacks() {
let cb;
const now = _now();
debuglog("_runCallbacks: now:", now);
// get the list of things to call
const callbacksToRun = [];
while (true) {
const first = _callbackList[0];
if (!first || first.runAt > now) {
break;
}
cb = _callbackList.shift();
debuglog("_runCallbacks: popping", cb.key);
callbacksToRun.push(cb);
}
// reschedule the real callback before running our functions, to
// keep the codepaths the same whether or not our functions
// register their own setTimeouts.
_scheduleRealCallback();
for (let i = 0; i < callbacksToRun.length; i++) {
cb = callbacksToRun[i];
try {
cb.func.apply(null, cb.params);
} catch (e) {
console.error("Uncaught exception in callback function",
e.stack || e);
}
}
}
/* search in a sorted array.
*
* returns the index of the last element for which func returns
* greater than zero, or array.length if no such element exists.
*/
function binarySearch(array, func) {
// min is inclusive, max exclusive.
let min = 0,
max = array.length;
while (min < max) {
const mid = (min + max) >> 1;
const res = func(array[mid]);
if (res > 0) {
// the element at 'mid' is too big; set it as the new max.
max = mid;
} else {
// the element at 'mid' is too small. 'min' is inclusive, so +1.
min = mid + 1;
}
}
// presumably, min==max now.
return min;
}
+42 -22
View File
@@ -1,13 +1,28 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module which manages queuing, scheduling and retrying
* of requests.
* @module scheduler
*/
var utils = require("./utils");
var q = require("q");
const utils = require("./utils");
const q = require("q");
var DEBUG = false; // set true to enable console logging.
const DEBUG = false; // set true to enable console logging.
/**
* Construct a scheduler for Matrix. Requires
@@ -45,7 +60,7 @@ function MatrixScheduler(retryAlgorithm, queueAlgorithm) {
* @see MatrixScheduler.removeEventFromQueue To remove an event from the queue.
*/
MatrixScheduler.prototype.getQueueForEvent = function(event) {
var name = this.queueAlgorithm(event);
const name = this.queueAlgorithm(event);
if (!name || !this._queues[name]) {
return null;
}
@@ -61,11 +76,11 @@ MatrixScheduler.prototype.getQueueForEvent = function(event) {
* @return {boolean} True if this event was removed.
*/
MatrixScheduler.prototype.removeEventFromQueue = function(event) {
var name = this.queueAlgorithm(event);
const name = this.queueAlgorithm(event);
if (!name || !this._queues[name]) {
return false;
}
var removed = false;
let removed = false;
utils.removeElement(this._queues[name], function(element) {
if (element.event.getId() === event.getId()) {
removed = true;
@@ -95,7 +110,7 @@ MatrixScheduler.prototype.setProcessFunction = function(fn) {
* resolved or rejected in due time, else null.
*/
MatrixScheduler.prototype.queueEvent = function(event) {
var queueName = this.queueAlgorithm(event);
const queueName = this.queueAlgorithm(event);
if (!queueName) {
return null;
}
@@ -103,15 +118,15 @@ MatrixScheduler.prototype.queueEvent = function(event) {
if (!this._queues[queueName]) {
this._queues[queueName] = [];
}
var defer = q.defer();
const defer = q.defer();
this._queues[queueName].push({
event: event,
defer: defer,
attempts: 0
attempts: 0,
});
debuglog(
"Queue algorithm dumped event %s into queue '%s'",
event.getId(), queueName
event.getId(), queueName,
);
_startProcessingQueues(this);
return defer.promise;
@@ -133,8 +148,14 @@ MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
// client error; no amount of retrying with save you now.
return -1;
}
// we ship with browser-request which returns { cors: rejected } when trying
// with no connection, so if we match that, give up since they have no conn.
if (err.cors === "rejected") {
return -1;
}
if (err.name === "M_LIMIT_EXCEEDED") {
var waitTime = err.data.retry_after_ms;
const waitTime = err.data.retry_after_ms;
if (waitTime) {
return waitTime;
}
@@ -180,10 +201,10 @@ function _startProcessingQueues(scheduler) {
function _processQueue(scheduler, queueName) {
// get head of queue
var obj = _peekNextEvent(scheduler, queueName);
const obj = _peekNextEvent(scheduler, queueName);
if (!obj) {
// queue is empty. Mark as inactive and stop recursing.
var index = scheduler._activeQueues.indexOf(queueName);
const index = scheduler._activeQueues.indexOf(queueName);
if (index >= 0) {
scheduler._activeQueues.splice(index, 1);
}
@@ -192,7 +213,7 @@ function _processQueue(scheduler, queueName) {
}
debuglog(
"Queue '%s' has %s pending events",
queueName, scheduler._queues[queueName].length
queueName, scheduler._queues[queueName].length,
);
// fire the process function and if it resolves, resolve the deferred. Else
// invoke the retry algorithm.
@@ -206,22 +227,21 @@ function _processQueue(scheduler, queueName) {
}, function(err) {
obj.attempts += 1;
// ask the retry algorithm when/if we should try again
var waitTimeMs = scheduler.retryAlgorithm(obj.event, obj.attempts, err);
const waitTimeMs = scheduler.retryAlgorithm(obj.event, obj.attempts, err);
debuglog(
"retry(%s) err=%s event_id=%s waitTime=%s",
obj.attempts, err, obj.event.getId(), waitTimeMs
obj.attempts, err, obj.event.getId(), waitTimeMs,
);
if (waitTimeMs === -1) { // give up (you quitter!)
debuglog(
"Queue '%s' giving up on event %s", queueName, obj.event.getId()
"Queue '%s' giving up on event %s", queueName, obj.event.getId(),
);
// remove this from the queue
_removeNextEvent(scheduler, queueName);
obj.defer.reject(err);
// process next event
_processQueue(scheduler, queueName);
}
else {
} else {
setTimeout(function() {
_processQueue(scheduler, queueName);
}, waitTimeMs);
@@ -230,7 +250,7 @@ function _processQueue(scheduler, queueName) {
}
function _peekNextEvent(scheduler, queueName) {
var queue = scheduler._queues[queueName];
const queue = scheduler._queues[queueName];
if (!utils.isArray(queue)) {
return null;
}
@@ -238,7 +258,7 @@ function _peekNextEvent(scheduler, queueName) {
}
function _removeNextEvent(scheduler, queueName) {
var queue = scheduler._queues[queueName];
const queue = scheduler._queues[queueName];
if (!utils.isArray(queue)) {
return null;
}
@@ -247,7 +267,7 @@ function _removeNextEvent(scheduler, queueName) {
function debuglog() {
if (DEBUG) {
console.log.apply(console, arguments);
console.log(...arguments);
}
}
+283
View File
@@ -0,0 +1,283 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* This is an internal module. See {@link MatrixInMemoryStore} for the public class.
* @module store/memory
*/
const utils = require("../utils");
const User = require("../models/user");
/**
* Construct a new in-memory data store for the Matrix Client.
* @constructor
* @param {Object=} opts Config options
* @param {LocalStorage} opts.localStorage The local storage instance to persist
* some forms of data such as tokens. Rooms will NOT be stored.
*/
module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
opts = opts || {};
this.rooms = {
// roomId: Room
};
this.users = {
// userId: User
};
this.syncToken = null;
this.filters = {
// userId: {
// filterId: Filter
// }
};
this.accountData = {
// type : content
};
this.localStorage = opts.localStorage;
};
module.exports.MatrixInMemoryStore.prototype = {
/**
* Retrieve the token to stream from.
* @return {string} The token or null.
*/
getSyncToken: function() {
return this.syncToken;
},
/**
* Set the token to stream from.
* @param {string} token The token to stream from.
*/
setSyncToken: function(token) {
this.syncToken = token;
},
/**
* Store the given room.
* @param {Room} room The room to be stored. All properties must be stored.
*/
storeRoom: function(room) {
this.rooms[room.roomId] = room;
// add listeners for room member changes so we can keep the room member
// map up-to-date.
room.currentState.on("RoomState.members", this._onRoomMember.bind(this));
// add existing members
const self = this;
room.currentState.getMembers().forEach(function(m) {
self._onRoomMember(null, room.currentState, m);
});
},
/**
* Called when a room member in a room being tracked by this store has been
* updated.
* @param {MatrixEvent} event
* @param {RoomState} state
* @param {RoomMember} member
*/
_onRoomMember: function(event, state, member) {
if (member.membership === "invite") {
// We do NOT add invited members because people love to typo user IDs
// which would then show up in these lists (!)
return;
}
const user = this.users[member.userId] || new User(member.userId);
if (member.name) {
user.setDisplayName(member.name);
if (member.events.member) {
user.setRawDisplayName(
member.events.member.getDirectionalContent().displayname,
);
}
}
if (member.events.member && member.events.member.getContent().avatar_url) {
user.setAvatarUrl(member.events.member.getContent().avatar_url);
}
this.users[user.userId] = user;
},
/**
* Retrieve a room by its' room ID.
* @param {string} roomId The room ID.
* @return {Room} The room or null.
*/
getRoom: function(roomId) {
return this.rooms[roomId] || null;
},
/**
* Retrieve all known rooms.
* @return {Room[]} A list of rooms, which may be empty.
*/
getRooms: function() {
return utils.values(this.rooms);
},
/**
* Permanently delete a room.
* @param {string} roomId
*/
removeRoom: function(roomId) {
if (this.rooms[roomId]) {
this.rooms[roomId].removeListener("RoomState.members", this._onRoomMember);
}
delete this.rooms[roomId];
},
/**
* Retrieve a summary of all the rooms.
* @return {RoomSummary[]} A summary of each room.
*/
getRoomSummaries: function() {
return utils.map(utils.values(this.rooms), function(room) {
return room.summary;
});
},
/**
* Store a User.
* @param {User} user The user to store.
*/
storeUser: function(user) {
this.users[user.userId] = user;
},
/**
* Retrieve a User by its' user ID.
* @param {string} userId The user ID.
* @return {User} The user or null.
*/
getUser: function(userId) {
return this.users[userId] || null;
},
/**
* Retrieve all known users.
* @return {User[]} A list of users, which may be empty.
*/
getUsers: function() {
return utils.values(this.users);
},
/**
* Retrieve scrollback for this room.
* @param {Room} room The matrix room
* @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.
*/
scrollback: function(room, limit) {
return [];
},
/**
* 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.
*/
storeEvents: function(room, events, token, toStart) {
// no-op because they've already been added to the room instance.
},
/**
* Store a filter.
* @param {Filter} filter
*/
storeFilter: function(filter) {
if (!filter) {
return;
}
if (!this.filters[filter.userId]) {
this.filters[filter.userId] = {};
}
this.filters[filter.userId][filter.filterId] = filter;
},
/**
* Retrieve a filter.
* @param {string} userId
* @param {string} filterId
* @return {?Filter} A filter or null.
*/
getFilter: function(userId, filterId) {
if (!this.filters[userId] || !this.filters[userId][filterId]) {
return null;
}
return this.filters[userId][filterId];
},
/**
* Retrieve a filter ID with the given name.
* @param {string} filterName The filter name.
* @return {?string} The filter ID or null.
*/
getFilterIdByName: function(filterName) {
if (!this.localStorage) {
return null;
}
try {
return this.localStorage.getItem("mxjssdk_memory_filter_" + filterName);
} catch (e) {}
return null;
},
/**
* Set a filter name to ID mapping.
* @param {string} filterName
* @param {string} filterId
*/
setFilterIdByName: function(filterName, filterId) {
if (!this.localStorage) {
return;
}
try {
this.localStorage.setItem("mxjssdk_memory_filter_" + filterName, filterId);
} catch (e) {}
},
/**
* Store user-scoped account data events.
* N.B. that account data only allows a single event per type, so multiple
* events with the same type will replace each other.
* @param {Array<MatrixEvent>} events The events to store.
*/
storeAccountDataEvents: function(events) {
const self = this;
events.forEach(function(event) {
self.accountData[event.getType()] = event;
});
},
/**
* Get account data event by event type
* @param {string} eventType The event type being queried
* @return {?MatrixEvent} the user account_data event of given type, if any
*/
getAccountData: function(eventType) {
return this.accountData[eventType];
},
// TODO
//setMaxHistoryPerRoom: function(maxHistory) {},
// TODO
//reapOldMessages: function() {},
};
+245
View File
@@ -0,0 +1,245 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module store/session/webstorage
*/
const utils = require("../../utils");
const DEBUG = false; // set true to enable console logging.
const E2E_PREFIX = "session.e2e.";
/**
* Construct a web storage session store, capable of storing account keys,
* session keys and access tokens.
* @constructor
* @param {WebStorage} webStore A web storage implementation, e.g.
* 'window.localStorage' or 'window.sessionStorage' or a custom implementation.
* @throws if the supplied 'store' does not meet the Storage interface of the
* WebStorage API.
*/
function WebStorageSessionStore(webStore) {
this.store = webStore;
if (!utils.isFunction(webStore.getItem) ||
!utils.isFunction(webStore.setItem) ||
!utils.isFunction(webStore.removeItem) ||
!utils.isFunction(webStore.key) ||
typeof(webStore.length) !== 'number'
) {
throw new Error(
"Supplied webStore does not meet the WebStorage API interface",
);
}
}
WebStorageSessionStore.prototype = {
/**
* Store the end to end account for the logged-in user.
* @param {string} account Base64 encoded account.
*/
storeEndToEndAccount: function(account) {
this.store.setItem(KEY_END_TO_END_ACCOUNT, account);
},
/**
* Load the end to end account for the logged-in user.
* @return {?string} Base64 encoded account.
*/
getEndToEndAccount: function() {
return this.store.getItem(KEY_END_TO_END_ACCOUNT);
},
/**
* Store a flag indicating that we have announced the new device.
*/
setDeviceAnnounced: function() {
this.store.setItem(KEY_END_TO_END_ANNOUNCED, "true");
},
/**
* Check if the "device announced" flag is set
*
* @return {boolean} true if the "device announced" flag has been set.
*/
getDeviceAnnounced: function() {
return this.store.getItem(KEY_END_TO_END_ANNOUNCED) == "true";
},
/**
* Stores the known devices for a user.
* @param {string} userId The user's ID.
* @param {object} devices A map from device ID to keys for the device.
*/
storeEndToEndDevicesForUser: function(userId, devices) {
setJsonItem(this.store, keyEndToEndDevicesForUser(userId), devices);
},
/**
* Retrieves the known devices for a user.
* @param {string} userId The user's ID.
* @return {object} A map from device ID to keys for the device.
*/
getEndToEndDevicesForUser: function(userId) {
return getJsonItem(this.store, keyEndToEndDevicesForUser(userId));
},
/**
* Store the sync token corresponding to the device list.
*
* This is used when starting the client, to get a list of the users who
* have changed their device list since the list time we were running.
*
* @param {String?} token
*/
storeEndToEndDeviceSyncToken: function(token) {
setJsonItem(this.store, KEY_END_TO_END_DEVICE_SYNC_TOKEN, token);
},
/**
* Get the sync token corresponding to the device list.
*
* @return {String?} token
*/
getEndToEndDeviceSyncToken: function() {
return getJsonItem(this.store, KEY_END_TO_END_DEVICE_SYNC_TOKEN);
},
/**
* Store a session between the logged-in user and another device
* @param {string} deviceKey The public key of the other device.
* @param {string} sessionId The ID for this end-to-end session.
* @param {string} session Base64 encoded end-to-end session.
*/
storeEndToEndSession: function(deviceKey, sessionId, session) {
const sessions = this.getEndToEndSessions(deviceKey) || {};
sessions[sessionId] = session;
setJsonItem(
this.store, keyEndToEndSessions(deviceKey), sessions,
);
},
/**
* Retrieve the end-to-end sessions between the logged-in user and another
* device.
* @param {string} deviceKey The public key of the other device.
* @return {object} A map from sessionId to Base64 end-to-end session.
*/
getEndToEndSessions: function(deviceKey) {
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
},
/**
* Retrieve a list of all known inbound group sessions
*
* @return {{senderKey: string, sessionId: string}}
*/
getAllEndToEndInboundGroupSessionKeys: function() {
const prefix = E2E_PREFIX + 'inboundgroupsessions/';
const result = [];
for (let i = 0; i < this.store.length; i++) {
const key = this.store.key(i);
if (!key.startsWith(prefix)) {
continue;
}
// we can't use split, as the components we are trying to split out
// might themselves contain '/' characters. We rely on the
// senderKey being a (32-byte) curve25519 key, base64-encoded
// (hence 43 characters long).
result.push({
senderKey: key.substr(prefix.length, 43),
sessionId: key.substr(prefix.length + 44),
});
}
return result;
},
getEndToEndInboundGroupSession: function(senderKey, sessionId) {
const key = keyEndToEndInboundGroupSession(senderKey, sessionId);
return this.store.getItem(key);
},
storeEndToEndInboundGroupSession: function(senderKey, sessionId, pickledSession) {
const key = keyEndToEndInboundGroupSession(senderKey, sessionId);
return this.store.setItem(key, pickledSession);
},
/**
* Store the end-to-end state for a room.
* @param {string} roomId The room's ID.
* @param {object} roomInfo The end-to-end info for the room.
*/
storeEndToEndRoom: function(roomId, roomInfo) {
setJsonItem(this.store, keyEndToEndRoom(roomId), roomInfo);
},
/**
* Get the end-to-end state for a room
* @param {string} roomId The room's ID.
* @return {object} The end-to-end info for the room.
*/
getEndToEndRoom: function(roomId) {
return getJsonItem(this.store, keyEndToEndRoom(roomId));
},
};
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
const KEY_END_TO_END_ANNOUNCED = E2E_PREFIX + "announced";
const KEY_END_TO_END_DEVICE_SYNC_TOKEN = E2E_PREFIX + "device_sync_token";
function keyEndToEndDevicesForUser(userId) {
return E2E_PREFIX + "devices/" + userId;
}
function keyEndToEndSessions(deviceKey) {
return E2E_PREFIX + "sessions/" + deviceKey;
}
function keyEndToEndInboundGroupSession(senderKey, sessionId) {
return E2E_PREFIX + "inboundgroupsessions/" + senderKey + "/" + sessionId;
}
function keyEndToEndRoom(roomId) {
return E2E_PREFIX + "rooms/" + roomId;
}
function getJsonItem(store, key) {
try {
// if the key is absent, store.getItem() returns null, and
// JSON.parse(null) === null, so this returns null.
return JSON.parse(store.getItem(key));
} catch (e) {
debuglog("Failed to get key %s: %s", key, e);
debuglog(e.stack);
}
return null;
}
function setJsonItem(store, key, val) {
store.setItem(key, JSON.stringify(val));
}
function debuglog() {
if (DEBUG) {
console.log(...arguments);
}
}
/** */
module.exports = WebStorageSessionStore;

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