Compare commits

..

783 Commits

Author SHA1 Message Date
David Baker ef594d52e4 v1.0.2 2019-03-18 13:47:54 +00:00
David Baker 23bbb2f8c6 Prepare changelog for v1.0.2 2019-03-18 13:47:53 +00:00
David Baker 70575f9e33 v1.0.2-rc.1 2019-03-13 14:24:57 +00:00
David Baker 8e16586d84 Merge branch 'develop' into release-v1.0.2 2019-03-13 14:24:26 +00:00
David Baker 6920dfb800 Yarn needs --new-version to create a new version 2019-03-13 14:23:51 +00:00
David Baker 02d93770aa Prepare changelog for v1.0.2-rc.1 2019-03-13 14:20:18 +00:00
David Baker cd124231c5 Merge pull request #858 from jryans/yarn-ci
Use modern Yarn version on Travis CI
2019-03-13 13:22:39 +00:00
J. Ryan Stinnett cd75848882 Use modern Yarn version on Travis CI
Travis CI uses a quite old version of Yarn by default. This adds Yarn's
recommended incantation for using the latest stable version.
2019-03-13 12:28:07 +00:00
J. Ryan Stinnett 1bae15ede9 Recommend using the latest Node LTS version 2019-03-13 10:50:58 +00:00
J. Ryan Stinnett 4570fcaa8a Ignore .npmrc 2019-03-12 12:02:04 +00:00
J. Ryan Stinnett 90670cf1be Merge pull request #856 from jryans/yarn
Switch to `yarn` for dependency management
2019-03-11 13:27:04 +00:00
J. Ryan Stinnett cc86f427d2 Convert Olm to a dev dependency
This gives more natural behavior of downloading Olm by default when working on
this module, so that all tests including crypto are run.
2019-03-08 15:58:08 +00:00
J. Ryan Stinnett 2144791d52 Update scripts and docs to use yarn where appropriate
Most `npm` operations are replaced with `yarn`, which generally has better
behavior. However, steps like publish that write to the NPM registry are left to
`npm`, which currently handles these tasks best.
2019-03-08 15:45:57 +00:00
J. Ryan Stinnett 33aabf44e7 Convert from npm to yarn lock file
The npm lock file was imported into yarn. A yarn install pass was then run to
double-check the lock file for sanity.
2019-03-08 10:47:58 +00:00
Hubert Chathi fc1ea27380 Merge pull request #855 from uhoreg/fix_key_requests
More key request fixes
2019-03-06 14:46:43 -05:00
Hubert Chathi 81946294d8 use lolex to fake the timer 2019-03-06 14:42:52 -05:00
Matthew Hodgson 77270fa78c Merge pull request #851 from matrix-org/travis/e2e-notifs
Calculate encrypted notification counts
2019-03-06 17:11:22 +00:00
Hubert Chathi 9e29289dcc use a different transaction ID when re-sending a key request 2019-03-06 12:02:48 -05:00
Travis Ralston 6198943976 Add a mention that we should be handling gaps in /sync 2019-03-06 09:56:36 -07:00
David Baker 8beb836ccd Merge pull request #854 from matrix-org/dbkr/deps_190206
Update dependencies
2019-03-06 14:08:26 +00:00
David Baker b7c0e39c1a Update dependencies
To get non-vulnerable versions
2019-03-06 13:50:35 +00:00
David Baker 777acae2e5 Merge branch 'master' into develop 2019-03-06 11:17:25 +00:00
David Baker e77389c1ce v1.0.1 2019-03-06 11:10:42 +00:00
David Baker eb24e2e1f1 Prepare changelog for v1.0.1 2019-03-06 11:10:41 +00:00
Hubert Chathi 114244f8bb fix typo 2019-03-06 00:23:01 -05:00
Travis Ralston 54769d9136 Add jsdoc 2019-03-05 14:25:32 -07:00
Travis Ralston 2f2deb5333 Expose the clear event content directly from an event 2019-03-05 14:04:51 -07:00
Travis Ralston 37f106d4af More safely set the push actions for an encrypted event 2019-03-05 14:04:39 -07:00
David Baker 36ee7cdbfc v1.0.1-rc.2 2019-03-05 18:53:16 +00:00
David Baker 2b564498ee Prepare changelog for v1.0.1-rc.2 2019-03-05 18:53:15 +00:00
David Baker 0ddba16fa1 Merge pull request #853 from matrix-org/bwindels/dontswallowcryptotxnerrors
dont swallow txn errors in crypto store
2019-03-05 18:36:15 +00:00
Hubert Chathi 550086eb67 Merge pull request #850 from uhoreg/ensure_key_request
make sure key requests get sent
2019-03-05 10:57:15 -05:00
Hubert Chathi 055ce673cd fix jsdoc 2019-03-05 10:54:02 -05:00
David Baker 5308595658 Merge pull request #852 from matrix-org/dbkr/getusermedia_deviceid_ideal
Use 'ideal' rather than 'exact' for deviceid
2019-03-05 14:23:39 +00:00
David Baker e726e29f39 Better logging here too 2019-03-05 13:02:55 +00:00
David Baker 33b12fa6b5 Use 'ideal' rather than 'exact' for deviceid
We were using 'exact' which means we fail outright if the device
we wanted isn't available. This means if a user selects a specific
device then later unplugs it, we fail to open a capture device
the next time they make a call even if there's one available.
Using 'ideal' uses the chosen device in preference, but something
else if it isn't available.

Also log the name of the exception when we fail to open a capture
device to give us more of an idea of what's gone wrong.

Should help fix https://github.com/vector-im/riot-web/issues/8993
2019-03-05 12:59:29 +00:00
Travis Ralston 829cd05cba Appease the linter 2019-03-04 21:46:48 -07:00
Travis Ralston 4834e12a3a Calculate unread badges for encrypted events 2019-03-04 21:41:20 -07:00
Travis Ralston bcd4ad130c Use the decrypted event content when checking the push rules
Otherwise we'll be looking at the encrypted source, and that doesn't help anyone.
2019-03-04 21:33:57 -07:00
Travis Ralston 998d9e010e Support flushing the cache on calculated push rules
Needed for encrypted events to be able to pass some push rules.
2019-03-04 21:24:25 -07:00
Hubert Chathi 5480e8e1d5 refactor key sharing requests
use sendRoomKeyRequest with a new resend flag, instead of cancelRoomKeyRequest,
when requesting keys, so that we make sure that we send a new request if there
is no previous request

fixes https://github.com/vector-im/riot-web/issues/6838
2019-03-04 17:09:56 -05:00
Hubert Chathi 98fdcabc00 stop client after each test 2019-03-04 16:59:54 -05:00
Bruno Windels 236397816d Merge pull request #849 from matrix-org/bwindels/dontswallowcryptotxnerrors
Don't swallow txn errors in crypto store
2019-03-04 13:05:17 +01:00
Bruno Windels 755c55de3e dont swallow txn errors in crypto store 2019-03-04 12:58:05 +01:00
Hubert Chathi 526da71992 Merge pull request #848 from uhoreg/fix_partial_keyshare
handle partially-shared sessions better
2019-03-01 12:18:54 -05:00
Hubert Chathi 86ef262799 fix c+p comment 2019-03-01 12:15:59 -05:00
Hubert Chathi 282904d4be restore the order of backup vs retry decryption 2019-03-01 08:54:04 -05:00
Hubert Chathi a1be24307a lint 2019-02-28 22:54:46 -05:00
Hubert Chathi 4b5623691b handle partially-shared sessions better
- don't cancel key requests if we can't decrypt everything in the session
- overwrite the session key if we get a better version
2019-02-28 16:01:29 -05:00
David Baker 7bdf1e9b92 v1.0.1-rc.1 2019-02-28 14:28:36 +00:00
David Baker af1db8a606 Prepare changelog for v1.0.1-rc.1 2019-02-28 14:28:36 +00:00
Travis Ralston a99bb3c4c9 Merge pull request #847 from matrix-org/travis/fix-megolm-error
Fix "e is undefined" masking the original error in MegolmDecryption
2019-02-26 13:59:16 -07:00
Travis Ralston fd155c15bd Excessive checks are excessive 2019-02-26 13:50:09 -07:00
Travis Ralston aaa43631aa Fix "e is undefined" masking the original error in MegolmDecryption 2019-02-26 13:15:03 -07:00
Hubert Chathi d2557bc943 only set the dirty flag if something was actually changed 2019-02-22 08:37:34 -05:00
Hubert Chathi 33a3506981 speling is hard 2019-02-15 17:14:29 -05:00
J. Ryan Stinnett 03a54353be v1.0.0 2019-02-14 16:48:12 +00:00
J. Ryan Stinnett c6328923e6 Prepare changelog for v1.0.0 2019-02-14 16:48:11 +00:00
J. Ryan Stinnett 1ecb820bb0 Merge branch 'develop' into release-v1.0.0 2019-02-14 16:46:34 +00:00
J. Ryan Stinnett 0be2319288 Merge pull request #841 from jryans/package-lock-release
Try again to commit package-lock.json
2019-02-14 15:57:21 +00:00
J. Ryan Stinnett 073a025b83 Try again to commit package-lock.json 2019-02-14 14:20:31 +00:00
J. Ryan Stinnett e83836d487 v1.0.0-rc.2 2019-02-14 10:47:41 +00:00
J. Ryan Stinnett 065c61e05c Prepare changelog for v1.0.0-rc.2 2019-02-14 10:47:41 +00:00
J. Ryan Stinnett 139a6bd903 Merge branch 'develop' into release-v1.0.0 2019-02-14 10:43:04 +00:00
J. Ryan Stinnett 3fa0ee59d4 Merge pull request #839 from matrix-org/dbkr/commit_package_lock
Release script: commit package-lock.json
2019-02-13 18:57:38 +00:00
David Baker bd3d26422d git st is not a standard thing 2019-02-13 18:20:16 +00:00
David Baker 370ef9fc69 Merge pull request #840 from matrix-org/dbkr/recheck_key_backup
Add method to force re-check of key backup
2019-02-13 18:19:35 +00:00
David Baker a087fb37a3 Add method to force re-check of key backup
Also detect when the key backup version changes and do the right
thing

https://github.com/vector-im/riot-web/issues/8524
2019-02-13 15:40:07 +00:00
David Baker 68c8fe0fa9 Release script: commit package-lock.json
Commit the package-lock.json when bumping the version, otherwise
the versions get out of sync, and this is going to matter more now
that jenkins runs `npm ci` which is fussy about these things.
2019-02-13 13:40:46 +00:00
Bruno Windels 4309749979 Merge pull request #838 from matrix-org/bwindels/e2eiconsanddialog
Fix: dont check for unverified devices in left members
2019-02-13 11:41:57 +01:00
Bruno Windels 1a677804a4 use getEncryptionTargetMembers instead of doing membership checks ourselves 2019-02-13 11:22:38 +01:00
Bruno Windels a427e2a75c dont check devices for left members 2019-02-12 18:33:44 +01:00
David Baker 3c735b0ac1 v1.0.0-rc.1 2019-02-08 18:33:52 +00:00
David Baker 4f446c3909 Prepare changelog for v1.0.0-rc.1 2019-02-08 18:33:51 +00:00
Hubert Chathi 999ed1b5b3 Merge pull request #837 from uhoreg/emoji_sas
change hex SAS verification to decimal and emoji
2019-02-08 11:44:53 -05:00
David Baker 8fa19f4a0f More en_GBification 2019-02-08 14:56:08 +00:00
David Baker 71a01ec234 Replace symbol characters with the emoji variants
The symbol ones are often rendered as little black & white icons: replace them with their variants from the emoji range

Also change to en_GB
2019-02-08 14:11:47 +00:00
David Baker 32f033a9da Merge pull request #836 from matrix-org/dbkr/trust_on_decrypt
Trust on decrypt
2019-02-08 11:36:44 +00:00
David Baker dade385147 Put error constant on the class 2019-02-08 10:43:17 +00:00
David Baker 6cf2e54f9a Fix double-santa 2019-02-08 09:59:42 +00:00
Hubert Chathi fb673b0304 change hex SAS verification to decimal and emoji 2019-02-08 00:56:30 -05:00
David Baker 1a425af3f2 Pass backup info here too 2019-02-07 15:51:51 +00:00
David Baker 9bafed2c26 OK fine, we'll use the session store 2019-02-07 15:33:49 +00:00
David Baker bb2d0b0f62 lint 2019-02-07 14:47:03 +00:00
David Baker 5e4f10a80c Trust on decrypt
Trust backups that we've restored by saving the matching pubkey
locally.

NB. Contains technically breaking API changes to the backup restore
(takes backupInfo rather than version).
2019-02-07 14:37:25 +00:00
David Baker 9e12fc4d7d Merge pull request #835 from matrix-org/dbkr/always_track_own_device_list
Always track our own devices
2019-02-07 09:30:13 +00:00
David Baker 1caf2b7f83 Always track our own devices
It's generally a reasonable assumption that we'll be interested in
them, and important for key backup.

Fixes https://github.com/vector-im/riot-web/issues/8213
2019-02-06 16:48:57 +00:00
David Baker 6f17e3e659 Merge pull request #834 from matrix-org/dbkr/lint_consistency
Make linting rules more consistent
2019-02-05 13:12:25 +00:00
David Baker 17e2cd755d Make linting rules more consistent
* Put back babel-eslint for class-properties
 * Allow arrow functions without params

This makes the style more consistent with react-sdk.

NB. The line lengths are still inconsistent but it's not clear which
way to go on that yet.
2019-02-05 11:58:53 +00:00
Bruno Windels a6970d4de8 Merge pull request #833 from matrix-org/bwindels/e2eicons
add method to room to check for unverified devices
2019-02-01 18:14:51 +00:00
Bruno Windels bc99c1f3ce add method to room to check for unverified devices 2019-02-01 14:37:11 +01:00
David Baker 5e3ff7fc27 Re-apply changelog that somehow got lost 2019-01-30 13:12:32 +00:00
Bruno Windels ffe3f966fe Merge pull request #831 from matrix-org/experimental
Merge redesign into develop
2019-01-30 12:47:33 +00:00
Travis Ralston c60c19a28e Merge pull request #830 from matrix-org/travis/rver-cap-support
Supporting infrastructure for educated decisions on when to upgrade rooms
2019-01-29 11:28:57 -07:00
Travis Ralston 4ea785b604 Add some prose for what safe versions are 2019-01-29 10:46:40 -07:00
Travis Ralston 2d4e9d0d3f Add safety for when the endpoint doesn't exist 2019-01-28 17:18:57 -07:00
Travis Ralston 971d572fbf Supporting infrastructure for educated decisions on when to upgrade rooms
Part of https://github.com/vector-im/riot-web/issues/8251
2019-01-28 16:03:27 -07:00
Hubert Chathi 244e1b84f7 Initial implementation of key verification 2019-01-23 13:34:25 -05:00
David Baker e5cdc99a34 Merge pull request #826 from matrix-org/dbkr/key_backup_status_unknown_device
Include signature info for unknown devices
2019-01-18 10:58:21 +00:00
David Baker 9a5768219f Doc function API changes 2019-01-18 10:04:22 +00:00
Travis Ralston cee8f57318 Merge pull request #828 from matrix-org/travis/v2-is-safe
Flag v2 rooms as "safe"
2019-01-17 16:36:27 -07:00
Travis Ralston 1a40e0a83a Flag v2 rooms as "safe"
We'll still need something like https://github.com/matrix-org/matrix-doc/pull/1804 to make this work correctly, but this fixes the immediate issue in https://github.com/vector-im/riot-web/issues/8154
2019-01-17 16:14:53 -07:00
Travis Ralston d0072d930f Merge pull request #827 from matrix-org/develop
Develop->Experimental
2019-01-17 15:51:22 -07:00
David Baker 385062c4d7 Include signature info for unknown devices
Add a 'deviceId' property and leave 'device' undefined for unknown
devices.

https://github.com/vector-im/riot-web/issues/8142
2019-01-17 16:43:26 +00:00
J. Ryan Stinnett 9245638b25 Set key backup guard flag to avoid duplicate uploads
Not sure how this was missed, but anyway, guard against duplicate uploads with
the existing flag.
2019-01-17 08:19:09 -06:00
J. Ryan Stinnett 1865542192 Emit when user's status message changes 2019-01-15 08:59:31 -06:00
J. Ryan Stinnett 2563abda11 Ensure we have power to set custom status
If we're in a non-DM room of 2 people, we may not have power to set state events
like custom status.  Ensure that we do before sending.
2019-01-15 08:59:31 -06:00
David Baker 59b80d8fbd Remove babel-eslint as we no longer need it 2019-01-10 10:42:43 +00:00
David Baker 68bb8182e4 Update ESLint
* Bump version
 * Add a couple of rules to match our existing precedent
 * Fix a few genuine lint errors
 * Ignore a guard-for-in (not sure why eslint doesn't like this?)
 * Update max warnings
2019-01-10 10:42:34 +00:00
David Baker c979ff6696 Merge pull request #821 from matrix-org/dbkr/update_eslint
Update ESLint
2019-01-10 09:35:24 +00:00
J. Ryan Stinnett 25681e888c Change initial key backup to background
Alters the APIs used for initial key backup so that the actual upload happens in
the background after all session are marked for backup.
2019-01-09 12:03:55 -06:00
J. Ryan Stinnett 5cfd082b00 Schedule key upload on enabling backup
This ensures a partially completed backup will continue to make progress.
2019-01-09 12:03:55 -06:00
J. Ryan Stinnett 0cbced43bd Emit when count of sessions to backup changes
This will be used in the React SDK to display upload progress when there are
many sessions to upload.
2019-01-09 12:03:55 -06:00
David Baker b3e8d7e07e Remove babel-eslint as we no longer need it 2019-01-09 17:33:42 +00:00
David Baker f4a7395e3a Update ESLint
* Bump version
 * Add a couple of rules to match our existing precedent
 * Fix a few genuine lint errors
 * Ignore a guard-for-in (not sure why eslint doesn't like this?)
 * Update max warnings
2019-01-09 17:29:30 +00:00
J. Ryan Stinnett 14b42abfa4 Re-check key backup status on version mismatch
This ensures we will report the updated status when consumer code asks for it.

Fixes part of https://github.com/vector-im/riot-web/issues/8048.
2019-01-09 04:07:58 -06:00
J. Ryan Stinnett e8022e985e Merge pull request #815 from matrix-org/develop
Merge develop into experimental
2019-01-03 13:54:34 -06:00
David Baker f6c8687dc8 Merge pull request #812 from matrix-org/dbkr/getallsessions
Add a getAllEndToEndSessions to crypto store
2019-01-03 10:09:18 +00:00
J. Ryan Stinnett 472d8faace Merge pull request #668 from matrix-org/t3chguy/fix_displayname_logic
T3chguy/fix displayname logic
2019-01-02 17:40:19 -06:00
Michael Telatynski fc5f3c2fcc re-add empty check after removing hidden chars 2018-12-30 00:20:33 +00:00
Travis Ralston fb756208d8 Merge pull request #814 from trashhalo/rebase-tip
Contributing: Note that rebase lets you mass signoff commits

replace find loop + slice with a single filter. More readable

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

keep public APIs consistent with previous behaviour now that its fuzzy

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

make tests not fail, because of order of occurrence

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

do falsey displayname check regardless of whether we have roomstate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

add tests for the fuzzy disambiguation between members in a room

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

remove duplicated call
2018-12-30 00:18:05 +00:00
Travis Ralston 6a98e93845 Merge pull request #814 from trashhalo/rebase-tip
Contributing: Note that rebase lets you mass signoff commits
2018-12-28 19:40:41 -07:00
Stephen Solka 79e155acfb Note that rebase lets you mass signoff commits
Signed-off-by: Stephen Solka <stephen0q@gmail.com>
2018-12-28 21:36:06 -05:00
David Baker 59ae6e3dc0 Missing bracket 2018-12-21 19:21:40 +00:00
David Baker e628ed3ef4 Add a getAllEndToEndSessions to crypto store
So we can migrate them from place to place
2018-12-21 19:12:01 +00:00
J. Ryan Stinnett 11d40e9daa Merge pull request #672 from matrix-org/t3chguy/unhomoglyph
take into account homoglyphs when calculating similar display names
2018-12-20 14:38:48 +00:00
Michael Telatynski a07f0631b7 Include unhomoglyph in package-lock.json - it must have felt left out :L
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-12-19 22:28:40 +00:00
Michael Telatynski 790d1dd8f7 Merge branches 'develop' and 't3chguy/unhomoglyph' of github.com:matrix-org/matrix-js-sdk into t3chguy/unhomoglyph 2018-12-19 22:21:09 +00:00
J. Ryan Stinnett c92e510a4d Pin to base-x 3.0.4
This avoids API changes in 3.0.5 (requiring `Buffer` instances), but more
importantly also avoids dealing with ES6 in dependencies for another day.

Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>
2018-12-18 19:47:17 +00:00
David Baker c48a6c0601 Merge pull request #809 from jryans/key-backup-version-change
Emit for key backup failures
2018-12-18 09:34:43 +00:00
J. Ryan Stinnett 383f3f9834 Merge branch 'develop' into t3chguy/unhomoglyph 2018-12-18 01:01:41 +00:00
J. Ryan Stinnett 3c1e9ba6e9 Merge pull request #646 from leonlianght/develop
emit oldEventId on "updatePendingEvent"
2018-12-18 00:41:52 +00:00
J. Ryan Stinnett 26893b9877 Merge pull request #589 from johbo/add-thirdparty-user
Add getThirdpartyUser to base api
2018-12-18 00:38:09 +00:00
J. Ryan Stinnett 2b734b8e69 Emit for key backup failures
Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>
2018-12-14 15:17:22 +00:00
J. Ryan Stinnett c5f6f87a6c Add await to ensure we wait for backup errors
Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>
2018-12-14 15:17:22 +00:00
David Baker 66cdb62a3d Reorganize key backup flow
This will allow the key backup flow to propagate errors for things like version
mismatches more easily.

In addition, it raises the limit of keys sent per request from 10 to 200 to cut
down on the number of requests.
2018-12-14 15:17:10 +00:00
Travis Ralston f53e33723b Merge pull request #805 from matrix-org/travis/custom-status
Support custom status messages
2018-12-13 10:24:37 -07:00
Travis Ralston 06bc6e7568 Update jsdoc to match _unstable_ prefix 2018-12-13 09:42:22 -07:00
J. Ryan Stinnett 5e3f42ec5a Use olm-loader in all crypto tests
Standardize on importing `olm-loader` rather than pasting the same boilerplate
in different tests.  Importantly, `spec/unit/crypto.spec.js` did not include any
loading approach, so it would only find Olm if some other test loaded it first.

Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>
2018-12-13 08:59:57 +00:00
Travis Ralston 08b3dfa3b5 Prefix the status message API with _unstable
It's not a formal feature of Matrix yet, so we should try and avoid people relying on it. This makes it appear as a private API and is very clearly labeled as not intended for use.
2018-12-12 23:05:03 -07:00
J. Ryan Stinnett 6cf9563441 Avoid checking key backup status if guest
Fixes vector-im/riot-web#7839.

Signed-off-by: J. Ryan Stinnett <jryans@gmail.com>
2018-12-12 22:26:01 +00:00
Travis Ralston fb65c7f4ba Support setting status message in rooms that look like 1:1s
Part of https://github.com/vector-im/riot-web/issues/1528
2018-12-12 13:21:13 -07:00
Travis Ralston c4452909e7 Support .well-known autodiscovery in the js-sdk (#799)
* Support .well-known autodiscovery in the js-sdk

It's much more useful here than in the react-sdk as it can be reused by more applications. This is also required to make the react-sdk a little easier to manage .well-known lookups as soon it'll be doing it in several places. 

Automatic discovery is an abstract concept in the spec and could include more than .well-known in the future, so this is made to be generic enough to support future mechanisms and other resources to discover. There's also a ton of comments (more than normally needed) as people may wish to use this as a reference in their own implementation and it doesn't hurt to explain what everything is doing.

Many of the functions are air lifted from the react-sdk and modified to work within the confines of the js-sdk.

* Swap out uglify-js for uglify-es

So we can start using ES6 dependencies without figuring out how to update babel. 

`uglify-es` is compatible with `uglify-js@3` (we were using `@2`) , which is why the same command is used. This commit includes changes to the command line to make the thing run the same as before too.

* Appease the linter

* Appease the linter some more

* Appease the linter: the tiebreaker

* Appease the linter yet again

* Switch to using the already available URL libraries

* Remove excess logging
2018-12-12 08:38:05 -07:00
Bruno Windels 2d3669b03b Merge pull request #749 from matrix-org/bwindels/releaseimprovements
Extra checks to avoid release script blowing up mid-process.
2018-12-12 09:07:12 +00:00
Travis Ralston 848e6e5897 Support reading custom status messages
Part of https://github.com/vector-im/riot-web/issues/1528
2018-12-11 21:41:15 -07:00
David Baker c723b76138 Merge branch 'master' into develop 2018-12-10 13:37:15 +00:00
David Baker 57f6b0af09 v0.14.2 2018-12-10 13:31:37 +00:00
David Baker 1c4082af45 Prepare changelog for v0.14.2 2018-12-10 13:31:37 +00:00
David Baker eece5d318e Merge pull request #800 from matrix-org/travis/mto-via
Move glob regex utilities out of the pushprocessor and into a more generic place
2018-12-07 17:47:56 +00:00
David Baker bb6ade2165 v0.14.2-rc.1 2018-12-06 11:11:29 +00:00
David Baker 586b010811 Prepare changelog for v0.14.2-rc.1 2018-12-06 11:11:28 +00:00
Travis Ralston 84ab0fde51 Appease the linter 2018-12-05 18:13:28 -07:00
Travis Ralston ec18df2c2a Move glob regex utilities out of the pushprocessor and into a more generic place 2018-12-05 18:01:12 -07:00
David Baker f50503e7c1 Merge pull request #794 from uhoreg/e2e_backup-test-fix
fix some assertions in e2e backup unit test
2018-12-04 11:22:20 +00:00
Travis Ralston c619e5c381 Merge pull request #798 from matrix-org/hs/fix-media-config
Config should be called with auth
2018-12-03 18:29:56 -07:00
Will Hunt e7c4a74ed6 Config should be called with auth 2018-12-04 01:27:08 +00:00
Hubert Chathi f8ea019f02 fix some assertions in e2e backup unit test 2018-11-29 11:52:19 -05:00
David Baker 6db8dd620d Fix https://github.com/matrix-org/matrix-js-sdk/pull/792 2018-11-28 16:11:28 +00:00
David Baker bdc1fa4c03 Merge pull request #792 from matrix-org/dbkr/dont_reestablish_unknwon_device
Don't re-establish sessions with unknown devices
2018-11-28 16:04:31 +00:00
David Baker 4e66a2d436 Don't re-establish sessions with unknown devices
as it won't work
2018-11-28 15:53:13 +00:00
David Baker 0fa948448e Merge branch 'master' into develop 2018-11-22 16:48:36 +00:00
David Baker 76c675cd09 v0.14.1 2018-11-22 16:46:26 +00:00
David Baker 85a4a594c5 Prepare changelog for v0.14.1 2018-11-22 16:46:25 +00:00
David Baker f70746c50f Handle crypto db version upgrades 2018-11-22 16:38:10 +00:00
David Baker 712490b671 Use a vaguely recent node 2018-11-21 18:44:21 +00:00
David Baker b580e68469 Merge pull request #736 from matrix-org/dbkr/e2e_backups
Support for e2e key backups
2018-11-21 18:27:45 +00:00
David Baker bd2cf18fbc Merge pull request #786 from matrix-org/dbkr/e2e_backups_passphrase
Passphrase Support for e2e backups
2018-11-21 18:24:59 +00:00
David Baker 092f4217b0 docs 2018-11-21 17:56:02 +00:00
David Baker abd2ac7168 Rename backup API call in test 2018-11-20 16:34:04 +00:00
David Baker eeea70640e Add randomString factored out from client secret 2018-11-20 16:28:29 +00:00
David Baker 6047838f53 lint 2018-11-20 16:17:58 +00:00
David Baker cb51799246 Make backup restore work 2018-11-20 16:15:29 +00:00
David Baker 44d99277fe Support passphrase-based e2e key backups 2018-11-20 13:09:59 +00:00
David Baker 5b8e643541 Merge branch 'master' into develop 2018-11-19 15:11:40 +00:00
David Baker ae85c209ab v0.14.0 2018-11-19 15:09:43 +00:00
David Baker 2306caa62f Prepare changelog for v0.14.0 2018-11-19 15:09:42 +00:00
Richard van der Hoff 17c11ae23f Merge pull request #783 from matrix-org/rav/sso_login_type
Add 'getSsoLoginUrl' function
2018-11-19 14:13:52 +01:00
Richard van der Hoff 5b51096e37 fix default login type for getSsoLoginUrl 2018-11-19 13:08:41 +00:00
Travis Ralston ac79d6bcee Merge pull request #784 from matrix-org/bwindels/missingheroescrash
Fix: don't set the room name to null when heroes are missing.
2018-11-16 13:23:57 -07:00
David Baker b6e056f832 Merge pull request #785 from matrix-org/dbkr/crypto_db_version_upgrade
Handle crypto db version upgrades
2018-11-16 17:32:40 +00:00
David Baker d99a22d68d Update to new API
Also fix test & remove debug logging from test
2018-11-16 14:46:18 +00:00
David Baker 2602c155d0 Handle crypto db version upgrades 2018-11-16 11:31:08 +00:00
Bruno Windels 80f562643f never return null as a name 2018-11-16 10:38:44 +01:00
Richard van der Hoff 578cb4e268 Add 'getSsoLoginUrl' function 2018-11-15 19:00:37 +00:00
David Baker c53c6a94d7 Update package-lock
so versions are consistent
2018-11-15 16:38:35 +00:00
David Baker 907cf19f05 Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-11-15 16:35:52 +00:00
David Baker 88682e1c3b Merge pull request #780 from matrix-org/dbkr/olm_session_unwedge
Restart broken Olm sessions
2018-11-15 16:34:10 +00:00
David Baker 20a4edf899 Merge pull request #776 from matrix-org/dbkr/use_session_last_received_message
Use the last olm session that got a message
2018-11-15 16:33:59 +00:00
David Baker 3222b11346 v0.14.0-rc.1 2018-11-15 15:06:39 +00:00
David Baker fc9d6a6d47 Prepare changelog for v0.14.0-rc.1 2018-11-15 15:06:39 +00:00
David Baker c9917e4079 lint 2018-11-15 10:03:16 +00:00
Bruno Windels b233ab87bb Merge branch 'master' into develop 2018-11-14 18:21:01 +01:00
Bruno Windels 73c3a709de Merge branch 'release-v0.13.1' 2018-11-14 18:19:24 +01:00
Bruno Windels 6ce7b30b72 v0.13.1 2018-11-14 18:18:12 +01:00
Bruno Windels 980d55a2f3 Prepare changelog for v0.13.1 2018-11-14 18:18:11 +01:00
Richard Lewis 988be62804 Add function to get currently joined room members. 2018-11-14 18:15:14 +01:00
Richard Lewis 23efd0850d Add function to get currently joined rooms. 2018-11-14 18:15:08 +01:00
David Baker 17e0f1d9ab Merge branch 'dbkr/use_session_last_received_message' into dbkr/olm_session_unwedge 2018-11-14 14:58:21 +00:00
David Baker 3c85bd55d3 Time goes forwards 2018-11-14 14:57:48 +00:00
David Baker 2298d72ab9 Merge branch 'dbkr/use_session_last_received_message' into dbkr/olm_session_unwedge 2018-11-14 14:35:33 +00:00
David Baker 408407b33d Fix typo 2018-11-14 14:34:36 +00:00
David Baker ab426384e1 Merge branch 'dbkr/use_session_last_received_message' into dbkr/olm_session_unwedge 2018-11-14 14:30:10 +00:00
David Baker 5bc68c0c6d Handle last received message ts being undefined 2018-11-14 14:29:03 +00:00
David Baker ebf20d5b2c Don't force more than one new session per device per hour 2018-11-14 14:20:55 +00:00
David Baker 93d9c40323 Merge branch 'dbkr/use_session_last_received_message' into dbkr/olm_session_unwedge 2018-11-14 14:20:42 +00:00
Bruno Windels c6ea976d7f Merge branch 'master' into develop 2018-11-14 10:55:13 +01:00
Bruno Windels 5f24915300 Merge branch 'release-v0.13.0' 2018-11-14 10:54:52 +01:00
Bruno Windels fbe174fb64 v0.13.0 2018-11-14 10:53:42 +01:00
Richard Lewis 977d5331c0 Update CHANGELOG. 2018-11-14 10:50:05 +01:00
Richard Lewis d40d7e18f5 Update CHANGELOG. 2018-11-14 10:49:54 +01:00
Richard Lewis 11be68ad49 Refactor code to base 'login' method. 2018-11-14 10:49:44 +01:00
Richard Lewis b0d0782a72 Linting. 2018-11-14 10:49:36 +01:00
Richard Lewis dbb6d8ac71 Set access_token and user_id after login in with username and password. 2018-11-14 10:49:25 +01:00
David Baker a30845f9ce lint 2018-11-14 08:03:23 +00:00
David Baker 379f290b8b Add package-lock.json
to force base-x to version 3.0.4 because 3.0.5 breaks the build
by exporting ES6.
2018-11-13 14:49:56 +00:00
David Baker 6c413bba48 Typo + exit if session was never shared 2018-11-13 12:15:33 +00:00
David Baker e17a39d446 PR feedback 2018-11-13 12:10:26 +00:00
David Baker fcadf6ec4a Store last received message ts on olm session 2018-11-12 18:12:43 +00:00
David Baker 231fde219c Store last received message ts on olm session 2018-11-12 18:10:11 +00:00
David Baker 2774bd238b Merge pull request #781 from matrix-org/travis/block-send-if-failed
Prevent messages from being sent if other messages have failed to send
2018-11-09 10:54:38 +00:00
Travis Ralston fed67192bc Fix test: Actually resend the event after unknown devices are found
This test didn't actually test that resending would work, despite its name.
2018-11-08 17:17:30 -07:00
Travis Ralston 16db970558 Appease the linter 2018-11-08 16:51:21 -07:00
Travis Ralston c9a79bf32e Prevent messages from being sent if other messages have failed to send
Fixes https://github.com/vector-im/riot-web/issues/5408
2018-11-08 16:46:03 -07:00
David Baker d74ed508f9 Restart broken Olm sessions
* Start a new Olm sessions with a device when we get an undecryptable
   message on it.
 * Send a dummy message on that sessions such that the other end knows
   about it.
 * Re-send any outstanding keyshare requests for that device.

Also includes a unit test for megolm that isn't very related but came
out as a result anyway.

Includes https://github.com/matrix-org/matrix-js-sdk/pull/776
Fixes https://github.com/vector-im/riot-web/issues/3822
2018-11-08 19:09:28 +00:00
David Baker eafba9c7ef Merge pull request #777 from matrix-org/dbkr/olm_unit_test
A unit test for olm
2018-11-06 12:04:55 +00:00
Richard Lewis 610923af89 Merge pull request #778 from matrix-org/rxl881/loginWithPassword
Set access_token and user_id after login in with username and password.
2018-11-05 17:25:31 +00:00
Richard Lewis 23dfeb13df Update CHANGELOG. 2018-11-05 17:17:22 +00:00
Richard Lewis f4abd7d027 Update CHANGELOG. 2018-11-05 17:06:39 +00:00
Richard Lewis b716e71784 Refactor code to base 'login' method. 2018-11-05 16:40:11 +00:00
Richard Lewis 094598196a Linting. 2018-11-05 16:02:30 +00:00
David Baker db1d1c49a0 Merge pull request #779 from matrix-org/rxl881/getRooms
Add function to get currently joined rooms.
2018-11-05 12:14:12 +00:00
David Baker ff4125c11e Remove unused stuff & comments 2018-11-05 09:39:46 +00:00
Richard Lewis a0d51803ed Add function to get currently joined room members. 2018-11-05 00:08:04 +00:00
Richard Lewis 3aabd63975 Add function to get currently joined rooms. 2018-11-04 21:49:17 +00:00
Richard Lewis 394e37f9ea Set access_token and user_id after login in with username and password. 2018-11-04 19:43:18 +00:00
Travis Ralston 369b88d6f8 Merge pull request #775 from matrix-org/travis/cleanup-request
Remove the request-only stuff we don't need anymore
2018-11-02 09:39:23 -06:00
David Baker ec8b3ae515 Lint (and also name the test right) 2018-11-02 12:33:46 +00:00
David Baker c94382b46c A unit test for olm
Megolm has plenty but none for just the olm layer by itself. I
was playing with getting session re-establishment to work and a
unit test came out.
2018-11-02 12:05:13 +00:00
David Baker 2a6a67c6cc Inbound session creation counts as a received message 2018-11-01 17:08:43 +00:00
David Baker 37f0a9ad7b Try tests on node 10 2018-11-01 13:54:41 +00:00
David Baker 28540ad50a Use the last olm session that got a message
Implements https://github.com/matrix-org/matrix-doc/pull/1596
For https://github.com/vector-im/riot-web/issues/3822
Requires https://github.com/matrix-org/olm-backup/pull/77 (+release)
2018-11-01 12:59:38 +00:00
David Baker 29d92d3e81 Lint 2018-10-31 20:05:21 +00:00
David Baker 0477f354c9 Fix key forwarded count
It's exported in snake case
2018-10-31 19:45:29 +00:00
David Baker c7a0c1402c refer to getAllEndToEndInboundGroupSessions for magic numbers 2018-10-31 19:39:07 +00:00
David Baker 2af5643243 Clarify comment 2018-10-31 19:37:19 +00:00
David Baker 5e9885946f random double linebreak 2018-10-31 19:36:30 +00:00
Travis Ralston e89879d8a6 Remove the request-only stuff we don't need anymore
This was introduced in https://github.com/matrix-org/matrix-react-sdk/pull/2250 but can be pulled out due to https://github.com/matrix-org/matrix-js-sdk/pull/770. See https://github.com/vector-im/riot-web/issues/7634 for more information about the future.
2018-10-31 13:07:31 -06:00
David Baker 2f219f83db Catch exceptions from backupGroupSession() 2018-10-31 18:46:02 +00:00
David Baker 63e9f794c7 Remove unnecessary if 2018-10-31 18:37:38 +00:00
David Baker 7c0b910d7a remove unnecessary isFinite check 2018-10-31 18:36:55 +00:00
David Baker c77ecad9a5 clarify comment 2018-10-31 18:34:49 +00:00
David Baker db2897cf1e Remove spurious interlopers 2018-10-31 18:33:31 +00:00
David Baker 5c5ce0dfe3 Typo 2018-10-31 18:32:48 +00:00
David Baker 6de213483c Change getDeviceByIdentityKey() to just the 2 arg version 2018-10-31 18:31:56 +00:00
David Baker f5846b89ea More modern loop syntax 2018-10-31 18:18:41 +00:00
David Baker c5e7bedb37 Conclusion: no, it shouldn't 2018-10-31 18:08:53 +00:00
David Baker 2b46c560c7 Add crypto. prefix to keyBackupStatus event 2018-10-31 18:07:12 +00:00
David Baker c6ad0665b5 factor out duplicated test code 2018-10-31 18:03:40 +00:00
David Baker 8ab84dee16 PR feedback 1/n 2018-10-31 17:40:17 +00:00
Travis Ralston 62b2c07be2 Merge pull request #770 from matrix-org/travis/fix-request
Manually construct query strings for browser-request instances
2018-10-30 15:18:33 -06:00
Travis Ralston 2fb29ae8fd Use the right query string lib 2018-10-30 14:59:57 -06:00
Travis Ralston b57e858ad1 We can't use arrow functions, apparently. 2018-10-30 14:27:51 -06:00
Travis Ralston 054aac17aa Just use the interface provided to us rather than hack in a flag 2018-10-30 14:25:25 -06:00
Travis Ralston 68b65dd357 Because uglify-js breaks everything 2018-10-30 14:12:46 -06:00
Travis Ralston f2881126cd Manually construct query strings for browser-request instances
Because `request` just doesn't work for us in the browser, but `browser-request` is fine despite us having to do our own query strings.

Fixes https://github.com/vector-im/riot-web/issues/7620
2018-10-30 13:59:29 -06:00
Bruno Windels 11968a5888 Merge pull request #769 from matrix-org/bwindels/initcryptoll
Fix: correctly check for crypto being present
2018-10-30 16:13:32 +00:00
Bruno Windels ad279dc566 correctly check for crypto being present 2018-10-30 16:05:44 +01:00
David Baker 2814932845 lint 2018-10-30 12:36:03 +00:00
David Baker a2430dbc53 Fix DeviceList index of users by identity key
Was causing all keys to be send as unverified
2018-10-30 12:29:44 +00:00
David Baker e51d2dd36a Fix a few e2e backup bits
* Don't _maybeSendKeyBackup() as soon as we enable them: we shouldn't
   have anything to send anyway until we mark all sessions for backup,
   which we do just afterwards, so leave that to trigger the upload
   (otherwise the uploading triggered by backupAll just returns
   straight away because a backup is already in progress).
 * Pass delay & retry params to _maybeSendKeyBackup(): we want the
   all-key upload to happen straight away so pass in delay=0, and
   we also don't want to retry on a timer if the the user is waiting.
 * If we fail due to an HTTP 400 or similar, don't swallow the error.
 * Use the right indexeddb store
2018-10-30 11:45:19 +00:00
David Baker 604af1ac8c Merge branch 'master' into develop 2018-10-29 14:01:26 +00:00
David Baker 68c6393eb2 v0.12.1 2018-10-29 13:59:39 +00:00
David Baker 4cbf9c7f47 Prepare changelog for v0.12.1 2018-10-29 13:59:39 +00:00
Travis Ralston 8bb3b75b1d Merge pull request #768 from aaronraimist/lint
Update babel-eslint to 8.1.1
2018-10-26 21:13:04 -06:00
Aaron Raimist a76f0c7cb4 Update babel-eslint to 8.1.1
Signed-off-by: Aaron Raimist <aaron@raim.ist>
2018-10-26 22:07:40 -05:00
Travis Ralston 01e31afcbd Merge pull request #764 from matrix-org/travis/permalink-routing
Support `request` in the browser and support supplying servers to try in joinRoom()
2018-10-26 14:22:51 -06:00
David Baker 3b2f2f922e Bump db version 2018-10-26 18:49:05 +01:00
Travis Ralston 64b83b3245 Merge branch 'develop' into travis/permalink-routing 2018-10-26 09:20:54 -06:00
David Baker 563e6b3cdd Fix jsdoc 2018-10-26 13:23:37 +01:00
David Baker 6518bff2ac Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-10-26 12:09:38 +01:00
David Baker 0e26247b53 Speed up time rather than increasing timeouts 2018-10-26 12:08:55 +01:00
David Baker e69f7dbc5f Merge pull request #767 from matrix-org/dbkr/loglevel_dep
loglevel should be a normal dependency
2018-10-26 11:26:15 +01:00
David Baker 4d0f6df89a alphabetical order 2018-10-26 11:23:05 +01:00
David Baker 6b184363a1 loglevel should be a normal dependency
rather than a dev dependency
2018-10-26 11:16:20 +01:00
David Baker b519069634 Merge pull request #766 from matrix-org/dbkr/stop_devicelist
Stop devicelist when client is stopped
2018-10-26 11:14:36 +01:00
Travis Ralston 1bd44a7427 Merge remote-tracking branch 'origin/develop' into travis/permalink-routing 2018-10-25 14:36:46 -06:00
Travis Ralston 568ff5a3f5 Appease the linter 2018-10-25 14:36:14 -06:00
David Baker a6bf40d4e2 We can always import these now 2018-10-25 19:21:29 +01:00
David Baker b3bb99d76a Stop client after backup tests 2018-10-25 19:11:43 +01:00
David Baker 243bab7036 Merge branch 'dbkr/stop_devicelist' into dbkr/e2e_backups 2018-10-25 19:03:57 +01:00
David Baker 88b39f4b67 Stop devicelist when client is stopped
To avoid the devicelist trying to save after the client has been
stopped

Hopefully will fix random test failures on node 11.
2018-10-25 19:00:03 +01:00
David Baker 5e8061f846 Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-10-25 17:33:48 +01:00
David Baker 870e96a1df Merge pull request #743 from matrix-org/dbkr/wasm
Update to WebAssembly-powered Olm
2018-10-25 15:58:18 +01:00
David Baker 59070c2af6 Merge remote-tracking branch 'origin/develop' into dbkr/wasm 2018-10-25 15:29:41 +01:00
David Baker cec8936728 Merge pull request #763 from Ryuno-Ki/logging-lib
Logging lib. Fixes #332
2018-10-25 15:28:47 +01:00
David Baker 14071b0d31 Merge pull request #765 from matrix-org/dbkr/mock_request_stop
Use new stop() method on matrix-mock-request
2018-10-25 15:16:08 +01:00
David Baker 57173e4385 Use mock-request 1.2.2 2018-10-25 15:01:05 +01:00
David Baker 997caad985 jsdoc 2018-10-25 14:43:17 +01:00
David Baker 2b752c0c02 Use new stop() method on matrix-mock-request
To finish all pending flushes between tests. This stops the unit
tests from hanging on node 11 when run in certain combinations.

Requires https://github.com/matrix-org/matrix-mock-request/pull/6
(so will need a release of matrix-mock-request before merging)
2018-10-25 14:29:25 +01:00
Travis Ralston 2cccb8b450 Install memfs because webpack is made of fail 2018-10-24 16:57:36 -06:00
Travis Ralston 0c540ac8de Re-add the querystring options 2018-10-24 16:36:12 -06:00
Travis Ralston 6033b7b886 Update request and browserify; Use request in the browser 2018-10-24 16:36:00 -06:00
Travis Ralston b67f8d1389 Merge branch 'develop' into travis/permalink-routing 2018-10-24 16:22:18 -06:00
André Jaenisch ae645ad9f0 Use Node.js module export, since ES6 export breaks build.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 21:52:50 +02:00
André Jaenisch 5b72509dac Fix broken build.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 21:12:51 +02:00
André Jaenisch 3ce42a096b Add Apache license banner.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 21:06:04 +02:00
André Jaenisch 8331c2f267 Use ES6 export instead of CommonJS.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 21:05:21 +02:00
André Jaenisch b3c9570b0f Remove 'use strict'.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 20:56:29 +02:00
André Jaenisch 9d5c877df9 Set loglevel to DEBUG to remain current behaviour.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 20:55:54 +02:00
André Jaenisch a8e2727473 Set level of logger instead of all of them.
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 20:55:22 +02:00
David Baker 4b9c6e6bd2 Merge remote-tracking branch 'origin/develop' into dbkr/wasm 2018-10-24 19:15:04 +01:00
David Baker d29ac088c0 retest 2018-10-24 18:55:04 +01:00
David Baker 3a316de9ef Update to Olm 3 here too 2018-10-24 17:40:58 +01:00
David Baker 40cb37e824 Update to Olm 3 2018-10-24 17:37:33 +01:00
David Baker f165b55a1d Merge branch 'e2e_backups' of git://github.com/uhoreg/matrix-js-sdk into uhoreg-e2e_backups 2018-10-24 17:15:36 +01:00
David Baker 84b91d4575 Update to Olm 3 2018-10-24 16:58:48 +01:00
David Baker f5832423f4 v0.12.1-rc.1 2018-10-24 11:14:59 +01:00
David Baker 73dd07aadf Prepare changelog for v0.12.1-rc.1 2018-10-24 11:14:59 +01:00
André Jaenisch 0f39a45734 Fixing module export of logger. Refs #332
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 01:44:10 +02:00
André Jaenisch f41060c39a Replace console.log with loglevel logger. Fixes #332
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 00:48:57 +02:00
André Jaenisch bbb8e12bac Create logger module. Refs #332
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 00:48:57 +02:00
André Jaenisch d0e1471c91 Added loglevel library. Refs #332
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2018-10-24 00:48:52 +02:00
Hubert Chathi 322ef1fd63 update backup algorithm name to agree with the proposal 2018-10-22 11:28:16 -04:00
Travis Ralston 47cb97bc60 Merge pull request #762 from aaronraimist/valid-package-json
Add repository type to package.json to make it valid
2018-10-20 21:55:48 -06:00
Aaron Raimist 8d35bea830 Add repository type to package.json to make it valid
Signed-off-by: Aaron Raimist <aaron@raim.ist>
2018-10-20 21:38:04 -05:00
Travis Ralston d8bcc4e3f1 Initial support for specifying which servers to try in joinRoom
This has a bug when using browser-request where the query string for `server_name: [a, b]` comes out as `?server_name=a,b` instead of `?server_name=a&server_name=b`. This is due to browser-request not supporting the same qs options as request, so the qsStringifyOptions do nothing.
2018-10-19 13:34:22 -06:00
Hubert Chathi 434ac86090 properly fill out the is_verified and first_message_index fields 2018-10-19 10:51:19 -04:00
Travis Ralston 1061026f96 Merge pull request #761 from Half-Shot/hs/upload-limits
Add getMediaConfig()
2018-10-16 09:04:08 -06:00
Will Hunt e638c49160 Merge remote-tracking branch 'upstream/develop' into hs/upload-limits 2018-10-16 11:32:21 +01:00
David Baker 5d84db9fb7 Merge branch 'release-v0.12.0' 2018-10-16 10:49:14 +01:00
David Baker 874bdea634 v0.12.0 2018-10-16 10:47:33 +01:00
David Baker 68497d3a1f Prepare changelog for v0.12.0 2018-10-16 10:47:32 +01:00
David Baker b9e198c172 Oops: remove debug logging 2018-10-15 11:39:39 +01:00
Hubert Chathi 40d0a82342 remove accidental change to eslintrc 2018-10-12 15:45:48 -04:00
Hubert Chathi d49c0a1bcb more de-linting and fixing 2018-10-12 14:28:31 -04:00
Hubert Chathi 91fb7b0a7c fix unit tests for backup recovery 2018-10-12 12:03:51 -04:00
Hubert Chathi 9b12c22823 de-lint plus some minor fixes 2018-10-12 10:38:10 -04:00
Hubert Chathi 3957006fae Merge remote-tracking branch 'upstream/dbkr/e2e_backups' into e2e_backups 2018-10-11 14:01:26 -04:00
David Baker 874029dff0 oops - fix changelog format in retrospect 2018-10-11 15:38:34 +01:00
David Baker 6aff3ed407 v0.12.0-rc.1 2018-10-11 14:42:29 +01:00
David Baker c0ae78ae82 Prepare changelog for v0.12.0-rc.1 2018-10-11 14:42:29 +01:00
David Baker 8b22f01ecd Merge pull request #760 from matrix-org/bwindels/fixleavesinmemberlist
never replace /sync'ed memberships with OOB ones
2018-10-11 14:01:38 +01:00
Bruno Windels 2ed694b041 remove supersedes OOB logic 2018-10-11 14:32:03 +02:00
Bruno Windels a0ef6ab811 typo 2018-10-11 14:14:42 +02:00
Bruno Windels d098b39024 never replace /sync'ed memberships with OOB ones 2018-10-11 14:11:40 +02:00
David Baker 3cf23f8a5c Document breaking change 2018-10-11 12:13:25 +01:00
Hubert Chathi fc59bc2992 add localstorage support for key backups 2018-10-10 19:32:07 -04:00
Hubert Chathi da65f43983 wrap backup sending in a try, and add delays 2018-10-10 19:31:28 -04:00
Bruno Windels 72e77d237a Merge pull request #759 from matrix-org/dbkr/lazy_load_check_before_sync
Don't fail to start up if lazy load check fails
2018-10-10 18:28:47 +02:00
David Baker ecc3e18e85 typo 2018-10-10 17:27:06 +01:00
David Baker dea70af889 remove debug logging 2018-10-10 17:06:26 +01:00
David Baker 30362091e5 Don't fail to start up if lazy load check fails
Do the lazy loading check in the batch of things we do before
starting a sync rather than at client start time, so we don't fail
to start the client if we can't hit the HS to determine LL support.

Fixes https://github.com/vector-im/riot-web/issues/7455
2018-10-10 16:59:36 +01:00
David Baker ada4b6ef16 Lint 2018-10-09 15:46:12 +01:00
David Baker 59e6066579 Replace base58check with a simple parity check
base58check seems way overcomplicated for this purpose (plus the
module was exporting an es6 file, breaking the js-sdk build). A
parity check empirically detects single substitution and transposition
errors. Another option would be Luhn's algorithm.
2018-10-09 14:15:03 +01:00
David Baker 0aa3362671 Merge pull request #754 from matrix-org/dbkr/e2e_on_interslice
Make e2e work on Edge
2018-10-09 10:48:24 +01:00
David Baker 5873db7331 Merge remote-tracking branch 'origin/develop' into dbkr/wasm 2018-10-09 10:47:11 +01:00
David Baker b3fe05ec81 Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-10-09 10:35:15 +01:00
David Baker 92fbf58b13 Merge pull request #758 from matrix-org/bwindels/betteridberrors
throw error with same name and message over idb worker boundary
2018-10-09 09:59:55 +01:00
Bruno Windels a4b2cc84c7 Merge pull request #755 from matrix-org/travis/fix-vuln-warning
Default to a room version of 1 when there is no room create event
2018-10-08 09:55:10 +02:00
David Baker 89c3f6fa0e Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-10-05 14:01:43 +01:00
David Baker 1395da694e Merge pull request #757 from matrix-org/dbkr/gone_away_is_the_bluebird
Silence bluebird warnings
2018-10-05 13:25:16 +01:00
David Baker 264b20535e Silence bluebird warnings 2018-10-05 12:13:05 +01:00
Bruno Windels caba350b33 throw error with same name and message over idb worker boundary
instead of string currently thrown. This allows handling error
from the main thread.
2018-10-05 12:48:12 +02:00
Travis Ralston d9fe194111 Default to a room version of 1 when there is no room create event
Fixes https://github.com/vector-im/riot-web/issues/7331

There is something to be worried about when there is no room create event, however. This should always be available, although due to cache problems or servers that don't provide the event we can't be sure of this.
2018-10-04 13:42:07 -06:00
Hubert Chathi 258adda67c retry key backups when they fail 2018-10-04 15:19:20 -04:00
David Baker 40dc13b2e2 lint try 2 2018-10-04 15:38:08 +01:00
David Baker 4cda54ca1c lint 2018-10-04 15:15:30 +01:00
David Baker 8116c5b3f7 Make e2e work on Edge
We were sucessfully opening indexeddb but any queries using compound
indicies were failing because Edge doesn't support them, so messages
were failing to decrypt with 'DataError'.

Try a dummy query at startup, so if it fails we fall back to a
different store (ie. end up using localstorage on Edge).
2018-10-04 13:49:32 +01:00
David Baker 35d584c67b Remove outdated comment 2018-10-04 13:05:45 +01:00
David Baker 9504cbcc4f Merge remote-tracking branch 'origin/develop' into dbkr/wasm 2018-10-04 11:19:37 +01:00
Bruno Windels 1dcc5127d0 Merge pull request #750 from matrix-org/bwindels/allownonffmerge
allow non-ff merge from release branch into master
2018-10-04 10:22:02 +02:00
Bruno Windels 6790699279 Merge pull request #739 from matrix-org/ben/new-examples
add new examples, to be expanded into a post
2018-10-04 10:21:36 +02:00
David Baker 85e3d7083c Merge pull request #751 from matrix-org/dbkr/indexeddb_errors
Reject with the actual error on indexeddb error
2018-10-03 16:22:26 +01:00
David Baker 262ace1773 commit the recovery key util file 2018-10-03 10:20:57 +01:00
David Baker 7cd101d8cb Fix recovery key format 2018-10-02 19:22:10 +01:00
David Baker ce2058aea9 Merge branch 'dbkr/wasm' into dbkr/e2e_backups 2018-10-02 16:54:36 +01:00
David Baker e9b0acaa8e Merge remote-tracking branch 'origin/develop' into dbkr/e2e_backups 2018-10-02 16:50:37 +01:00
David Baker bd2da08c4e Reject with the actual error on indexeddb error
Rather than the event
2018-10-02 16:48:27 +01:00
Bruno Windels 0a88d419c6 allow non-ff merge from release branch into master 2018-10-01 17:21:01 +02:00
Bruno Windels 80c190db36 Merge branch 'master' into develop 2018-10-01 15:40:16 +02:00
Bruno Windels 550cf00ee7 Merge branch 'release-v0.11.1' 2018-10-01 15:36:37 +02:00
Ben Parsons fbca7951fc improvements suggested by Bruno 2018-10-01 14:29:24 +01:00
Bruno Windels 1e1358fcef v0.11.1 2018-10-01 15:23:37 +02:00
Bruno Windels fd1b3329f5 Prepare changelog for v0.11.1 2018-10-01 15:23:37 +02:00
Bruno Windels 9c9d8468a5 Merge pull request #744 from matrix-org/dbkr/update_mocha
Update mocha to v5
2018-09-28 12:57:49 +01:00
Bruno Windels 55ca03f100 make release compatible with latest release of hub (2.5) 2018-09-28 12:51:31 +02:00
Bruno Windels 83708725b2 check youre logged in with correct npm user when releasing 2018-09-28 11:16:00 +02:00
David Baker 6f59d62e5c Merge pull request #748 from matrix-org/bwindels/nollforguests
disable lazy loading for guests as they cant create filters
2018-09-27 19:29:54 +01:00
Bruno Windels 1c348f0cdb disable lazy loading for guests as they cant create filters 2018-09-27 18:55:21 +01:00
Bruno Windels 634596257d v0.11.1-rc.1 2018-09-27 11:47:28 +01:00
Bruno Windels 5e4973a1dc changelog 2018-09-27 11:45:33 +01:00
Bruno Windels 19f023e0ee Revert "v0.11.1-rc.1"
This reverts commit 090c15fe19.
2018-09-27 11:42:19 +01:00
Bruno Windels 090c15fe19 v0.11.1-rc.1 2018-09-27 11:38:25 +01:00
Bruno Windels e8b307dc4f Prepare changelog for v0.11.1-rc.1 2018-09-27 11:38:25 +01:00
Bruno Windels 056479d450 Revert "v0.11.1-rc.1"
This reverts commit 847d40e567.
2018-09-27 11:35:52 +01:00
Bruno Windels e5ebe2f888 Merge pull request #747 from matrix-org/bwindels/releasehubversion
make usage of hub compatible with latest version (2.5)
2018-09-27 11:30:17 +01:00
Bruno Windels e8e1b431ad make usage of hub compatible with latest version (2.5) 2018-09-27 11:28:01 +01:00
Bruno Windels 847d40e567 v0.11.1-rc.1 2018-09-27 11:20:21 +01:00
Bruno Windels cf6c555e6a Prepare changelog for v0.11.1-rc.1 2018-09-27 11:20:21 +01:00
Bruno Windels b508aa9ebc Merge pull request #746 from matrix-org/bwindels/resynconlltoggle
Detect when lazy loading has been toggled in client.startClient
2018-09-27 09:49:01 +01:00
Bruno Windels 5e7634506e Merge branch 'develop' into bwindels/resynconlltoggle 2018-09-26 18:23:07 +01:00
Bruno Windels ba39b64ced re-enable test 2018-09-26 18:00:40 +01:00
David Baker 33ad65a105 Don't assume Olm will be available from start
By doing `Olm = global.Olm` on script load, we require that Olm is
available right from the start, which isn't really necessary. As
long as it appears some time before we actually want to use it,
this is fine (we can probably assume it's not going to go away
again..?)

This means Riot doesn't need to faff about making sure olm is
loaded before starting anything else.
2018-09-26 16:39:22 +01:00
Travis Ralston fcebe89f6c Merge branch 'hs/upload-limits' into develop 2018-09-26 09:14:12 -06:00
Bruno Windels 2d5eb920b8 pass lazy loading flag into error, to format message based on it 2018-09-26 16:12:30 +01:00
Travis Ralston 26de2c86ed Merge pull request #745 from matrix-org/revert-644-hs/upload-limits
Revert "Add getMediaLimits to client"
2018-09-26 09:12:03 -06:00
Travis Ralston cba1e95d0a Revert "Add getMediaLimits to client" 2018-09-26 09:11:28 -06:00
Bruno Windels 78a5a88638 fix jsdoc lint and better naming 2018-09-26 12:49:33 +01:00
Bruno Windels b7b9c67259 fix lint 2018-09-26 12:49:26 +01:00
Bruno Windels 54bff81470 clear sync data on toggling LL,also throw spec. error and delegate clear
the sync data needs to be cleared toggling in both directions:
 not LL -> LL: you want to get rid of all the excess state in the
               sync data to get the RAM benefits
 LL -> not LL: you want to fill the sync data state again
               because getOutOfBandMembers won't be called
2018-09-26 12:39:56 +01:00
David Baker fe21972f4a Update mocha to v5
Mostly to get the non-vulnerable version of node-growl
2018-09-26 11:36:06 +01:00
Bruno Windels 58e3c72446 only store serializable options (string, boolean, number) 2018-09-26 11:34:58 +01:00
Bruno Windels 6dd5c6c317 fix existing missing this 2018-09-26 11:33:19 +01:00
Bruno Windels 4e0af3eafe don't return the IDBEvent from storeClientOptions
as it's not needed and not cloneable
2018-09-26 11:32:43 +01:00
Bruno Windels 1d0791142c all store methods should return a promise 2018-09-26 11:32:11 +01:00
Bruno Windels 2560ba2980 dont clear the store if its a brand new one 2018-09-26 10:37:52 +01:00
Bruno Windels 19be3dd852 fix lint 2018-09-26 10:13:40 +01:00
Travis Ralston 40f2a6978b Merge pull request #644 from Half-Shot/hs/upload-limits
Add getMediaLimits to client
2018-09-25 18:12:55 -06:00
Bruno Windels 1fd8c43d94 fix tests 2018-09-25 18:50:09 +01:00
David Baker 63cc3fd890 lint 2018-09-25 18:14:11 +01:00
David Baker c556ca40b1 Support Olm with WebAssembly
wasm Olm has a new interface: it now has an init method that needs
to be called and the promise it returns waited on before the Olm
module is used. Support that, and allow Crypto etc to be imported
whether Olm is enabled or not. Change whether olm is enabled to
be async since now it will be unavailable if the async module init
fails. Don't call getOlmVersion() until the Olm.init() is done.
2018-09-25 17:49:54 +01:00
Matthew Hodgson 3e32f47903 Merge pull request #742 from matrix-org/travis/build-process
Split npm start into an init and watch script
2018-09-25 17:31:23 +01:00
Travis Ralston 8f2824186a Split npm start into an init and watch script
This is to better support riot-web's build process without losing the functionality supplied by `npm start`. The watch script no longer performs an initial build and thus `start:init` has been created for this purpose.
2018-09-25 10:09:58 -06:00
Bruno Windels b0dbb20e22 fixup of in memory stores 2018-09-25 15:53:40 +01:00
Bruno Windels 0519c4c6b1 await startClient and use promises also so error gets shown 2018-09-25 15:53:14 +01:00
Bruno Windels 28184b4a29 check if lazy loading was enabled before in startClient 2018-09-25 15:32:10 +01:00
Bruno Windels 76175abea2 allow storing client options in indexeddb
so we can tell what options the sync data was created with
2018-09-25 15:30:35 +01:00
Ben Parsons d28f829b1c add new examples, to be expanded into a post 2018-09-20 13:12:50 +01:00
Bruno Windels 184c3dce15 Merge pull request #738 from matrix-org/revert-733-bwindels/roomnamealias
Revert "room name should only take canonical alias into account"
2018-09-20 12:23:38 +02:00
David Baker a08a3078da Revert "room name should only take canonical alias into account" 2018-09-20 11:20:49 +01:00
David Baker c2100d7622 Merge pull request #737 from matrix-org/bwindels/fixnamedisambiguation
fix display name disambiguation with LL
2018-09-20 10:22:38 +01:00
Bruno Windels a91fa59174 fix display name disambiguation with LL 2018-09-19 18:14:18 +02:00
Bruno Windels 574a6b68ae Merge pull request #735 from matrix-org/bwindels/fixstalerr
Introduce Room.myMembership event
2018-09-19 13:35:11 +02:00
David Baker 2f4c1dfcc4 Test all 3 code paths on backup restore 2018-09-18 17:33:47 +01:00
David Baker 1b62a21dbd Free PkEncryption/Decryption objects 2018-09-18 16:12:37 +01:00
David Baker a78825eff9 Bump to Olm 2.3.0 for PkEncryption 2018-09-18 15:06:28 +01:00
David Baker 0bad7b213e Fix lint
Remove commented code block as it's not immediately obvious it makes
sense or is the right way of suggesting a key restore.
2018-09-18 14:56:11 +01:00
David Baker e4bb37b1a8 Fix lint mostly 2018-09-18 14:53:59 +01:00
David Baker 54c443ac68 Make tests pass 2018-09-18 14:48:02 +01:00
David Baker 3af9af96ea More linting 2018-09-17 19:31:37 +01:00
David Baker f75d188131 Soe progress on linting 2018-09-17 19:25:42 +01:00
Bruno Windels fc3a00054f add test for new event 2018-09-17 19:33:36 +02:00
Bruno Windels 84e41c2ade fix tests 2018-09-17 18:28:07 +02:00
Bruno Windels 4630733b55 don't fall back anymore to member, as this is more reliable 2018-09-17 18:23:48 +02:00
Bruno Windels eb690e14e4 introduce Room.myMembership event
As you don't always have your own member with lazy loading
of members enabled, looking at the sync response section
where a room appears is the most reliable way of determining
the syncing user's membership in a room.

Before we already used this to read the
current room membership with `room.getMyMembership()`,
but we were still using the `RoomMember.membership` event
to detect when the syncing user's membership changed.

This event will help make those checks work well with LL enabled.
2018-09-17 18:20:49 +02:00
David Baker 009430e829 Add isValidRecoveryKey
Add method to check if a given string is a valid recovery key
2018-09-17 17:04:29 +01:00
David Baker 073fb73ff3 Make multi-room key restore work 2018-09-17 15:59:37 +01:00
David Baker e789747834 Check sigs on e2e backup & enable it if we can 2018-09-14 17:06:27 +01:00
David Baker 833002f846 Merge pull request #733 from matrix-org/bwindels/roomnamealias
room name should only take canonical alias into account
2018-09-14 11:38:26 +01:00
David Baker 3838fab788 WIP e2e key backup support
Continues from uhoreg's branch
2018-09-13 17:01:05 +01:00
Bruno Windels 907e9fc476 fix lint 2018-09-13 10:04:31 +02:00
Bruno Windels b829a39cd2 fix tests 2018-09-13 09:59:20 +02:00
Bruno Windels daa7af0605 room name should only take canonical alias into account 2018-09-13 09:52:21 +02:00
David Baker 47a1adc864 Merge pull request #732 from matrix-org/bwindels/fixcontextstatenotwrapped
state events from context response were not wrapped in a MatrixEvent
2018-09-11 13:27:03 +01:00
Bruno Windels 98e448acdd state events from context response were not wrapped in a MatrixEvent 2018-09-11 14:13:35 +02:00
David Baker 72bd51f26e Merge remote-tracking branch 'origin/develop' into uhoreg-e2e_backups 2018-09-11 12:02:47 +01:00
David Baker 29db856322 Merge branch 'e2e_backups' of git://github.com/uhoreg/matrix-js-sdk into uhoreg-e2e_backups 2018-09-11 12:00:46 +01:00
David Baker cd5cda916f Merge branch 'master' into develop 2018-09-10 11:35:25 +01:00
David Baker 33a1139772 v0.11.0 2018-09-10 11:33:51 +01:00
David Baker c5b62903f3 Prepare changelog for v0.11.0 2018-09-10 11:33:50 +01:00
David Baker 387fd16b40 v0.11.0-rc.1 2018-09-07 14:28:45 +01:00
David Baker c91b67d370 Prepare changelog for v0.11.0-rc.1 2018-09-07 14:28:44 +01:00
David Baker b809d1c263 Merge pull request #724 from matrix-org/bwindels/fastermemberinsert
Reduce amount of promises created when inserting members
2018-09-07 13:57:44 +01:00
David Baker 7bb6675abf Merge pull request #726 from matrix-org/bwindels/dontwaitforstorage
dont wait for LL members to be stored to resolve the members
2018-09-07 13:57:20 +01:00
David Baker b91bea94f4 Merge pull request #728 from matrix-org/bwindels/wrongemitargs
RoomState.members emitted with wrong argument order for OOB members
2018-09-07 13:57:09 +01:00
David Baker 96e21700bd Merge pull request #727 from matrix-org/dbkr/conn_did_fail
Only emit CATCHUP if recovering from conn error
2018-09-07 13:31:17 +01:00
David Baker 7e8f25bce3 Include 404 in connectivity success error codes 2018-09-07 13:18:45 +01:00
Bruno Windels 9e02049b05 RoomState.members emitted with wrong argument order for OOB members 2018-09-07 14:11:27 +02:00
David Baker affdfccd60 Only emit CATCHUP if recovering from conn error
Have the keepalive promise return a boolean indicating whether it
detected a connectivity failure or not. Use this to only emit
CATCHUP if there was a connectivity error, to try & suppress
the state flip-flopping back & forth between CATCHUP and ERROR
in the case where we have connectivity but the sync is returning
and error for whatever reason.
2018-09-07 11:29:44 +01:00
David Baker 402f8c27a9 Merge pull request #725 from matrix-org/dbkr/fix_sync_error_doc
Fix docstring for sync data.error
2018-09-07 09:44:28 +01:00
Bruno Windels ba4dc6c60a dont wait for LL members to be stored to resolve the members
this can easily add up to 100ms / 1000 of members
2018-09-07 10:44:06 +02:00
David Baker 6b8dd42547 Fox docstring for sync data.error
It's 'error', not 'err'
2018-09-06 19:53:52 +01:00
Bruno Windels 1511a27f4c update/remove comments 2018-09-06 18:09:46 +02:00
Bruno Windels 7ae6c147fa lint doesnt like async 2018-09-06 18:02:19 +02:00
Bruno Windels f51630eb07 dont create a promise for every inserted member but await the transaction instead 2018-09-06 18:01:20 +02:00
Bruno Windels e3586411e0 Merge pull request #723 from matrix-org/revert-721-revert-717-bwindels/fixllroompermission
Re-apply "Don't rely on members to query if syncing user can post to room"
2018-09-06 14:55:24 +02:00
David Baker a0639a32c7 Revert "Revert "Don't rely on members to query if syncing user can post to room"" 2018-09-06 11:47:40 +01:00
David Baker 759c6e77a7 Merge pull request #721 from matrix-org/revert-717-bwindels/fixllroompermission
Revert "Don't rely on members to query if syncing user can post to room"
2018-09-05 18:09:59 +01:00
David Baker 04ad3d7c3c Revert "Don't rely on members to query if syncing user can post to room" 2018-09-05 18:04:19 +01:00
Bruno Windels 49badd9a2f Merge pull request #717 from matrix-org/bwindels/fixllroompermission
Don't rely on members to query if syncing user can post to room
2018-09-05 13:07:29 +02:00
Bruno Windels 8b00083bca check power levels without relying on membership
as this might not be known for the syncing user.
instead, add a method to room which always knows the syncing user's membership
2018-09-05 12:36:53 +02:00
Bruno Windels 3d98e324b5 Merge pull request #719 from matrix-org/bwindels/fixavatars-parttrois
Fixes for room.guessDMUserId
2018-09-04 18:17:39 +02:00
Bruno Windels 768c66313f remove unneeded async keywords 2018-09-04 18:09:47 +02:00
Bruno Windels 3561fd1c05 Merge pull request #716 from matrix-org/bwindels/fixfilepanel
Fix filepanel also filtering main timeline with LL turned on.
2018-09-04 17:10:49 +02:00
Bruno Windels a6c055b6d1 Merge pull request #711 from matrix-org/bwindels/clearidbmembersonleave
Remove lazy loaded members when leaving room
2018-09-04 16:53:56 +02:00
Bruno Windels 0d24c18fed add comment to explain fix 2018-09-04 16:51:36 +02:00
Bruno Windels dd8b2a79fb Merge pull request #702 from matrix-org/bwindels/fixreconnectspinner
Fix: show spinner again while recovering from connection error
2018-09-04 16:39:38 +02:00
Bruno Windels f0095611bc add new CATCHUP state as breaking change 2018-09-04 16:38:46 +02:00
Bruno Windels a3567f0918 some tests for room.guessDMUserId() 2018-09-04 13:01:45 +02:00
Bruno Windels f0d3d0d74e remove unneeded checks, we should always have enough heroes or members 2018-09-04 13:01:15 +02:00
Bruno Windels 3e32bc0d5d check heroes is not falsy first, this would fail without LL 2018-09-04 13:00:34 +02:00
Bruno Windels 632e4aa120 pick the first member, dont need an array 2018-09-04 13:00:00 +02:00
Bruno Windels 2391ce198d this method returns userId, not member 2018-09-04 12:59:37 +02:00
David Baker e5e2bbd482 Merge branch 'master' into develop 2018-09-03 14:11:12 +01:00
David Baker 0b6632123b v0.10.9 2018-09-03 14:09:24 +01:00
David Baker b1801fc953 Prepare changelog for v0.10.9 2018-09-03 14:09:24 +01:00
Bruno Windels ca1a1c4f28 Merge pull request #714 from matrix-org/bwindels/memberlist-spinner
Add method to query LL state in client
2018-09-03 15:05:13 +02:00
Bruno Windels 3363cc4f1d shallow-clone the filter, so the timeline filter doesnt get written into it later on 2018-09-03 15:02:38 +02:00
Bruno Windels f84684982f add method to query LL state in client 2018-09-03 11:14:23 +02:00
Bruno Windels 3bed5969bf remove count logging, approach confirmed to work and be according to idb spec 2018-09-03 10:27:00 +02:00
Bruno Windels ebc162e3d8 do onLeft (which clears the LL members) as late as possible
to avoid chance that something might call loadMembersIfNeeded
on the room and load them back again.
2018-08-31 16:13:34 +02:00
Bruno Windels f30136dba3 only clear promise at the end to avoid race between load and clear members 2018-08-31 16:12:54 +02:00
Bruno Windels 9b1926f902 also clear out lazy loaded members from storage 2018-08-31 16:11:37 +02:00
David Baker d29524ba3f Merge pull request #707 from matrix-org/bwindels/llinvites
Fix: also load invited members when lazy loading members
2018-08-31 14:09:42 +01:00
Bruno Windels 7258fe4e5c clear out of band members in store when leaving room 2018-08-31 14:42:15 +02:00
Bruno Windels f8ea1702f8 store support for removing out of band members for a room 2018-08-31 14:42:15 +02:00
David Baker 7582c28c1a v0.10.9-rc.2 2018-08-31 13:40:37 +01:00
David Baker 5042eb87e7 Prepare changelog for v0.10.9-rc.2 2018-08-31 13:40:36 +01:00
David Baker e8e80732ef Merge pull request #710 from matrix-org/bwindels/avatarfixbis
Fix for "otherMember.getAvatarUrl is not a function"
2018-08-31 13:32:25 +01:00
David Baker b8744a79ae Merge pull request #708 from matrix-org/bwindels/avatarfixbis
Fix for "otherMember.getAvatarUrl is not a function"
2018-08-31 10:34:29 +01:00
Bruno Windels 414b153d28 also fallback to getting avatar from user 2018-08-31 11:05:06 +02:00
Bruno Windels 8e160dda8e make sure getAvatarFallbackMember always returns a member 2018-08-31 11:03:26 +02:00
David Baker 9b54c9b807 Merge pull request #704 from matrix-org/dbkr/discardsession
Pass through function to discard megolm session
2018-08-30 18:25:50 +01:00
Bruno Windels 1bb608cdb6 remove filter for LL members so invite members are also sent 2018-08-30 17:29:16 +02:00
Bruno Windels 1239485b30 fix test 2018-08-30 15:42:15 +02:00
Bruno Windels 0d23d047fc use CATCHUP state after ERROR before going back to SYNCING 2018-08-30 15:37:05 +02:00
Bruno Windels d837ae64ac triple = 2018-08-30 15:37:05 +02:00
Bruno Windels d72a70396a Pass through PREPARED state after error, when keepalive returns succes.
This is according to the state diagram in client.js.
This will show a spinner at the bottom of a room again
while the catchup sync is in progress,
which seems to have broken at some point.
2018-08-30 15:37:05 +02:00
David Baker 938772b86a v0.10.9-rc.1 2018-08-30 14:16:14 +01:00
David Baker 3e88593a81 Prepare changelog for v0.10.9-rc.1 2018-08-30 14:16:14 +01:00
David Baker 60c01d7869 Revert b0b0291 and a6de395
To make tests pass again
2018-08-30 12:03:53 +01:00
David Baker 1cbcc61bd6 Merge pull request #706 from matrix-org/bwindels/fixdmavatar
Fix DM avatar
2018-08-30 11:43:07 +01:00
Bruno Windels 7f5a2974ce allow self chats 2018-08-30 12:37:13 +02:00
Bruno Windels 3de3ea38b9 check heroes is present 2018-08-30 11:47:17 +02:00
Bruno Windels 3659e86d57 fix lint, actually get members as well 2018-08-30 11:41:35 +02:00
Bruno Windels c335a6b3de guess DM user id, used to patch up incorrect m.direct account data 2018-08-30 11:05:31 +02:00
Bruno Windels 267b831bc4 calculate fallback avatar for rooms with <= members but not DM 2018-08-30 11:05:05 +02:00
Bruno Windels 7ee93cb910 make sure our user is not in the summary heroes.
No reason to think it is right now, but if there is a server bug
we could end up showing your own avatar for a DM again.

Also convenience method as we add up invited + join count often
2018-08-30 11:04:32 +02:00
Matthew Hodgson ae95a49618 spell out m.new_devices no longer exist 2018-08-29 20:20:52 +01:00
David Baker 8f98504183 jsdoc 2018-08-29 18:11:53 +01:00
David Baker 1b77ee0ef4 Pass through function to discard megolm session
To make debugging crypto slightly faster
2018-08-29 18:06:45 +01:00
Matthew Hodgson a6de395cde unbreak tests from b0b0291bc7 2018-08-28 18:03:59 +01:00
Bruno Windels fcd6dd34b2 Merge pull request #699 from matrix-org/bwindels/fixlle2ememberfetch-bis
Lazy loading: avoid loading members at initial sync for e2e rooms
2018-08-28 15:36:42 +02:00
Bruno Windels a6ebfe4215 typo 2018-08-28 15:31:20 +02:00
Bruno Windels 6a9158aa62 Merge pull request #700 from matrix-org/bwindels/fixllmegolmsession
Improve setRoomEncryption guard against multiple m.room.encryption st…
2018-08-28 15:30:16 +02:00
Matthew Hodgson b0b0291bc7 hopefully fix invite_room_state as per https://github.com/vector-im/riot-web/issues/7229 2018-08-28 01:27:20 +01:00
Bruno Windels 85f1da1f10 revert unnecesary changes 2018-08-27 12:09:10 +02:00
Bruno Windels c47445ca98 no need to just add a space now 2018-08-27 12:01:22 +02:00
Bruno Windels 4e25867548 revert to async event processing
without LL, we could refresh the device list before all members have been tracked.
as promises, even resolved ones (in case of no LL), always continue async
2018-08-27 11:56:46 +02:00
Bruno Windels ad71bb30ac add comment back as we kept flag in the end 2018-08-27 11:51:29 +02:00
Bruno Windels 362bf1895d restore inhibitDeviceQuery param to avoid breaking change 2018-08-27 11:12:00 +02:00
Bruno Windels 7d00c0bd5a make LL/non-LL flow in Crypto more alike by always going through _roomDeviceTrackingState 2018-08-27 10:54:08 +02:00
Bruno Windels 5e5994f166 try and fix tests 2018-08-27 10:54:08 +02:00
Bruno Windels 7247762b60 Also support not lazy-loading members in Crypto 2018-08-27 10:54:08 +02:00
Bruno Windels 21e0c79f7d Revert "Revert "Lazy loading: don't block on setting up room crypto""
This reverts commit 5cf2ebea4f.
2018-08-27 10:54:08 +02:00
Bruno Windels 78b08bfef2 fix var declaration 2018-08-27 10:48:11 +02:00
Bruno Windels ae7e90dc2f do config comparison first to keep original error message 2018-08-27 10:39:14 +02:00
Hubert Chathi bf873bde42 split the backup version creation into two different methods 2018-08-24 22:13:13 -04:00
Hubert Chathi 017f81e430 fix some bugs 2018-08-24 16:39:22 -04:00
Bruno Windels 0028bfbfc7 fix lint 2018-08-24 18:42:48 +02:00
Bruno Windels 60c9c403bd Improve setRoomEncryption guard against multiple m.room.encryption state events
we were only bailing out when receiving a non JSON-identical m.room.encryption event.
When receiving an identical event, the algorithm in _roomEncryptors would be reset,
generating a new megolm session every time this happens (there is a LL synapse bug
where this happens on every sync).

As the _roomList is backed by indexeddb you might already have a config without the algorithm being present though,
so we first check for the room encryptor algorithm being present. If so, always bail out as setRoomEncryption was
already called for the given room.

If no algorithm is present, still check if the config is not being changed.
Also setup the roomlist and room encryption synchronously before awaiting
the indexeddb operation to store the room encryption config in roomlist.
2018-08-24 18:28:38 +02:00
David Baker ec5fff2046 Merge branch 'e2e_backups' of git://github.com/uhoreg/matrix-js-sdk into uhoreg-e2e_backups 2018-08-24 13:29:29 +01:00
Bruno Windels a7199a3d0d Merge pull request #698 from matrix-org/revert-696-bwindels/fixlle2ememberfetch
Revert "Lazy loading: don't block on setting up room crypto"
2018-08-23 14:10:13 +02:00
Bruno Windels 5cf2ebea4f Revert "Lazy loading: don't block on setting up room crypto" 2018-08-23 14:05:57 +02:00
David Baker 580e95605e Merge pull request #696 from matrix-org/bwindels/fixlle2ememberfetch
Lazy loading: don't block on setting up room crypto
2018-08-23 12:41:33 +01:00
David Baker 8c3d1df3cf Merge pull request #695 from matrix-org/dbkr/hide_replaced_rooms
Add getVisibleRooms()
2018-08-23 09:34:39 +01:00
David Baker 7c66f91429 Typo 2018-08-23 09:34:23 +01:00
Hubert Chathi 73e294b1bd add copyright header to backup.spec 2018-08-23 00:29:29 -04:00
Hubert Chathi e5ec479923 check that crypto is enabled 2018-08-23 00:27:30 -04:00
Hubert Chathi 75107f99b2 pass in key rather than decryption object to restoreKeyBackups 2018-08-23 00:26:21 -04:00
Hubert Chathi fb8efe368a initial draft of API for working with backup versions 2018-08-23 00:03:36 -04:00
Hubert Chathi 1faf477537 fix formatting and fix authedRequest usage 2018-08-22 23:58:59 -04:00
Bruno Windels e3d108454c fix test 2018-08-22 23:25:37 +02:00
Bruno Windels 806b40727d fix lint 2018-08-22 23:00:27 +02:00
Bruno Windels fa702efe8f fix typo 2018-08-22 19:22:50 +02:00
Bruno Windels 344e3e18ab start tracking room devices in background after finishing loading members 2018-08-22 19:13:18 +02:00
Bruno Windels aea9eaa307 Only start tracking devices in an e2e room when needed
This way we can put off loading the members
2018-08-22 18:00:38 +02:00
Bruno Windels dffe0b39b6 Merge pull request #697 from matrix-org/dbkr/joined_member_count_wrapper
Add wrapper around getJoinedMemberCount()
2018-08-22 17:44:59 +02:00
Bruno Windels 267d660527 add invite count to room as well 2018-08-22 17:42:30 +02:00
David Baker 4c3046f917 Add wrapper around getJoinedMemberCount()
On Room, because it's super confusing that Room has
getJoinedMembers() but not getJoinedMemberCount()

https://github.com/matrix-org/matrix-react-sdk/pull/2126 had assumed
that this method was on Room
2018-08-22 16:34:06 +01:00
Bruno Windels 71444b638b don't block on setting up room crypto
this will load members in case of LL and could take quite some time
The two async actions performed by onCryptoEvent is saving the crypto config
to the roomlist store and fetching the room members to track.
Both are guarded against double calls so not awaiting this should be fine.
2018-08-22 14:45:36 +02:00
David Baker 962ec7bb53 Add getVisibleRooms()
To hide rooms that have been replaced

For https://github.com/vector-im/riot-web/issues/7164
2018-08-22 11:58:44 +01:00
Bruno Windels 52fad6aec2 Merge pull request #694 from matrix-org/hs/fetch-room-event-api
Api to fetch events via /room/.../event/..
2018-08-21 14:39:14 +02:00
Will Hunt 5b830f0b6a Add fetchRoomEvent to base-apis.js 2018-08-21 11:07:48 +01:00
Will Hunt 3d24c8768f Drop fetchRoomEvent from client.js 2018-08-21 11:07:37 +01:00
Bruno Windels 977b8625f8 Merge pull request #693 from matrix-org/dbkr/room_upgrades
Support for room upgrades
2018-08-21 11:22:08 +02:00
Will Hunt df7dc04a1d Happy linter, happy developer 2018-08-20 14:16:47 +01:00
David Baker 269d3cb086 Merge branch 'master' into develop 2018-08-20 13:49:20 +01:00
David Baker 4d310cd461 v0.10.8 2018-08-20 13:44:30 +01:00
David Baker 88c5c39fcb Prepare changelog for v0.10.8 2018-08-20 13:44:29 +01:00
Will Hunt 79ca68300c Add callback 2018-08-20 12:47:26 +01:00
Will Hunt baca20b225 Add support for /rooms/$roomId/event/$eventId 2018-08-20 11:14:45 +01:00
David Baker 0e3cb1977f lint 2018-08-17 15:00:54 +01:00
David Baker 8b1fa72877 Copyright & debadgering 2018-08-17 14:59:34 +01:00
David Baker e8610a35b4 Support for room upgrades
For https://github.com/vector-im/riot-web/issues/7164
2018-08-17 14:55:12 +01:00
Bruno Windels 77e6442f73 Merge pull request #691 from matrix-org/bwindels/feature_lazyloading
Lazy loading of room members
2018-08-16 18:14:33 +02:00
David Baker eeddfd4919 v0.10.8-rc.1 2018-08-16 15:17:36 +01:00
David Baker fa16da86b3 Prepare changelog for v0.10.8-rc.1 2018-08-16 15:17:35 +01:00
Bruno Windels 372a628cab fix log whitespace 2018-08-15 12:01:26 +02:00
Bruno Windels 2f4d8c3530 check with server if it supports member lazy loading 2018-08-15 12:01:26 +02:00
Bruno Windels 482eab0e2a fix tests 2018-08-15 12:01:26 +02:00
Bruno Windels 03c63d9b12 use sync token for /members request, as synapse expects it now 2018-08-15 12:01:26 +02:00
Bruno Windels 07e87915ba fix and add tests 2018-08-15 12:01:26 +02:00
Bruno Windels 91f2bf99c0 fix lint 2018-08-15 12:01:26 +02:00
Bruno Windels 535d59db4d fixup 2018-08-15 12:01:26 +02:00
Bruno Windels 9739c3355a undo postponing tracking device keys on turning on room encryption 2018-08-15 12:01:26 +02:00
Bruno Windels 827db37eef fixup 2018-08-15 12:01:26 +02:00
Bruno Windels 0c6e47a5bc await for LL members in getEncryptionTargetUsers 2018-08-15 12:01:26 +02:00
Bruno Windels 864ea749e5 Move /members fetching to room as getEncryptionTargetMembers needs it 2018-08-15 12:01:26 +02:00
Bruno Windels 5d92ec3b7b prevent deadlock on startup
when loading the encrypted events from storage,
the code would wait for the encryption target users,
which would never come because you would only load them
when viewing the room.

This disabled starting to track the devices in the room
when the inhibitDeviceQuery is set.
2018-08-15 12:01:26 +02:00
Bruno Windels 733a3ed102 Defer encryption targeted users when OOB member loading hasn't started 2018-08-15 12:01:26 +02:00
Bruno Windels b14be026b7 pass LL flag to room, to know if we should wait for lazy members at all 2018-08-15 12:01:26 +02:00
Bruno Windels b4afe97289 spelling 2018-08-15 12:01:26 +02:00
Bruno Windels 43a7a607b2 fixup 2018-08-15 12:01:26 +02:00
Bruno Windels a8bf66d8af Make Room.getEncryptionTargetMembers async, as members might be loading 2018-08-15 12:01:26 +02:00
Bruno Windels 3616a07dbb store /members promise on room while loading members 2018-08-15 12:01:26 +02:00
Bruno Windels 6609dfd410 initial support for lazy loading when calling /context 2018-08-15 12:01:26 +02:00
Bruno Windels e3913bd397 Fix: missed call site while renaming prependStateEvents 2018-08-15 12:01:26 +02:00
Bruno Windels 8c01ed1469 add comments explaining why we ignore the put promise result 2018-08-15 12:01:26 +02:00
Bruno Windels 7aa0dcc89f PR feedback, rename method 2018-08-15 12:01:26 +02:00
Bruno Windels 5285b22a76 bump version as making startClient async is a breaking change 2018-08-15 12:01:26 +02:00
Bruno Windels 0fa49bc2cd PR feedback 2018-08-15 12:00:38 +02:00
Bruno Windels 01d8730850 cleanup, lint and docs 2018-08-15 12:00:38 +02:00
Bruno Windels 52149ce74a Move LL filter creation inside MatrixClient
As we need an option to turn lazy loading on (we can't just accept a filter,
as /messages has an incompatible filter), better only pass the option
and create the filter inside startClient
2018-08-15 12:00:38 +02:00
Bruno Windels bffc20612d Fix: member avatar was always forward looking
applying itself all the way till the next member event
when back paginating
2018-08-15 12:00:38 +02:00
Bruno Windels 2c0eb19a27 Fix sentinels changing from underneath us!
As RoomMember contains the event in a nested object (events.member),
a shallow copy was not enough to be immutable.

This solution won't copy OOB flags but that's not neccesary
for sentinels.
2018-08-15 12:00:38 +02:00
Bruno Windels 748c4737f6 remove dead code 2018-08-15 12:00:38 +02:00
Bruno Windels 769d5113f7 prepend state from /messages to appropriate timeline/room state (excluding event context for now) 2018-08-15 12:00:38 +02:00
Bruno Windels 157be6da05 centralize creating a /messages request
so we only need to add LL filter once
2018-08-15 12:00:38 +02:00
Bruno Windels 1dc4b8bb63 add option for lazy loading to startClient
we need more than just a filter, which is what is passed in now,
so have an explicit option. For now still take the filter but later on
this could be created inside MatrixClient
2018-08-15 12:00:38 +02:00
Bruno Windels f261599435 fix lint 2018-08-15 12:00:38 +02:00
Bruno Windels 2862b49057 Only return hero in getDMInviter if we were invited 2018-08-15 12:00:38 +02:00
Bruno Windels a8d0d8f33d provide method on room to help with DM detection with fallback to summary heroes/counts. 2018-08-15 12:00:38 +02:00
Bruno Windels f55a2079bf replace getMember(myId).membership with getMyMembership
This works with rooms which haven't had their members
loaded yet.
2018-08-15 12:00:38 +02:00
Bruno Windels c0f706a2a2 move userId into room 2018-08-15 12:00:38 +02:00
Bruno Windels b034f67a0f add oob member methods to stub store 2018-08-15 12:00:38 +02:00
Bruno Windels 977b9eb686 implement memory store methods 2018-08-15 12:00:38 +02:00
Bruno Windels 5e11bf735e store OOB status along with members, to avoid unneccesary fetching
for some small rooms, it is possible that calling /members would not
yield any previously unknown members, as they were all recently active.
This would be the case for most DMs.

For these rooms, we'd end up with 0 OOB members after lazy loading them,
so when getting them out of storage we need a way to distuinguist this case
from never having lazy loaded the members of the room at all.

We store a marker object in the same store and return [] or null accordingly.
This way the /members don't get fetched a second time.
2018-08-15 12:00:38 +02:00
Bruno Windels a8c73f7a4d add logging, should be useful as long as not merged into develop 2018-08-15 12:00:38 +02:00
Bruno Windels 86105611fc we dont need a separate index? 2018-08-15 12:00:38 +02:00
Bruno Windels 0364af7337 update indexeddb store to store member events, not profile information 2018-08-15 12:00:38 +02:00
Bruno Windels c618ce4625 store only out of band members 2018-08-15 12:00:38 +02:00
Bruno Windels 2b9c834476 add comment to clarify how we avoid race 2018-08-15 12:00:38 +02:00
Bruno Windels d366ec9c48 prototype how we could store ll members 2018-08-15 12:00:38 +02:00
Bruno Windels ca3981fba8 back-port infinite spinner fix as room state code changed considerably for lazy loading 2018-08-15 12:00:38 +02:00
Bruno Windels bb490faefe fix lint 2018-08-15 11:59:40 +02:00
Bruno Windels d8f673ed51 make sure invited count cache gets reset when updating member 2018-08-15 11:59:40 +02:00
Bruno Windels 6ce7170cf4 counts from summary api should override count members manually as members might not be complete 2018-08-15 11:59:40 +02:00
Bruno Windels 1d71e7243f no need to create new array here 2018-08-15 11:59:40 +02:00
Bruno Windels cf08901d02 fix lint 2018-08-15 11:59:40 +02:00
Bruno Windels 230a9311a0 actually need to subtract one from join+invite count as that includes the syncing user 2018-08-15 11:59:40 +02:00
Bruno Windels 576f7142c1 just need the member names actually 2018-08-15 11:59:40 +02:00
Bruno Windels 20b4285849 add some tests for room name based on room summary + fix because it was actually broken 2018-08-15 11:59:40 +02:00
Bruno Windels d67bdbf088 test accumulating summary fields 2018-08-15 11:59:40 +02:00
Bruno Windels 3a389793ff fix sync accumulator test to include summary 2018-08-15 11:59:40 +02:00
Bruno Windels f5ff5dc3e0 Fix name recalculation tests by not relying on mocking
I tried keeping the mocking but it would take too much
boilerplate code to make the tests work again, and even more
to write the tests for room name with lazy loading.

Just testing everything with a real implementation is not really
a unit test any more, but proved way easier.

It'll be somewhat annoying these tests will fail if there is
something wrong in roomstate (not room), but that's the trade-off
2018-08-15 11:59:40 +02:00
Bruno Windels 00bf5bdf69 unify member a bit access towards getMember
some tests for mock getMember, some for .members
if you use either in the code (as I did for room display name changes)
tests start playing and you play whack-a-mole switching between
both ways of accessing the members in a room.

lets start using one way so mocking becomes easier,
and besides, accessing an object internal members is not the best idea.
2018-08-15 11:59:40 +02:00
Bruno Windels 9541aa7dbf fix lint 2018-08-15 11:59:40 +02:00
Bruno Windels e61c6b89c8 bring room name calculation in line with summary spec, while maintaining some backwards compatibility 2018-08-15 11:59:40 +02:00
Bruno Windels a5b3869e9f add invited count, only copy summary fields if present in summary
only copy any member from summary as
they are only in the response when they change.
Also accumulate them in the sync accumulator
2018-08-15 11:59:40 +02:00
Bruno Windels fbdce27db2 m.heros => m.heroes 2018-08-15 11:59:40 +02:00
Bruno Windels 148876f597 sorting should happen always 2018-08-15 11:59:40 +02:00
Bruno Windels 0cb533beca no need to recalculate name here, as recalculate already does this 2018-08-15 11:59:40 +02:00
Bruno Windels 5811ebd6f3 Support summary heroes in room name calculation
Also clean-up algorithm, and remove assumption
that we have all members as much as possible
2018-08-15 11:59:40 +02:00
Bruno Windels 8fa87f8ba5 make room summary available to Room
from either the sync accumulator or the /sync endpoint
2018-08-15 11:59:40 +02:00
Bruno Windels 21ba4f71f6 jsdoc doesn't like generic promise type annotations 2018-08-15 11:59:40 +02:00
Bruno Windels 097e7df7c9 fix lint 2018-08-15 11:59:40 +02:00
Bruno Windels 83c6615d6e move me || syncedmembership code into room.getmymembership 2018-08-15 11:59:40 +02:00
Bruno Windels f6fafeaafb store membership from during sync, because we might not have own membership
to determine where a room should show up in the room list, we need to know
our membership type. But with lazy loading, we might not have our own member
if we weren't recently active in the room. Using getSyncedMembership can be
used to fallback if the users membership is not yet available.
2018-08-15 11:59:40 +02:00
Bruno Windels 420a88c776 remove obsolete flag 2018-08-15 11:59:40 +02:00
Bruno Windels 5fcf9481b3 fix room not having access to event mapper + tests 2018-08-15 11:59:39 +02:00
Bruno Windels 48c3dcc08a fix lint & fix and add tests 2018-08-15 11:59:39 +02:00
Bruno Windels 62333b3e2c Use /members api for lazy loading
This commit is a substantial change, as /members returns state events,
not profile information as /joined_members, and this allows to simplify
the implementation quite a bit. We can assume again all members have
a state event associated with it.

I also changed most of the naming of lazy loaded members to
out-of-band members to reflect that this is the relevant bit for most
of the code, that the members didn't come through /sync but through
another channel.

This commit also addresses the race condition between /(joined_)members
and /sync. /members returns the members at the point in the timeline
at a given event id. Members are loaded at the last event
in the live timeline, and all members that come in from sync
in the mean time are marked as superseding the out of band members,
so they won't be overwritten, even if the timeline is reset in the
mean time.

Members are also marked if they originate from an out-of-band channel
(/members) so they can be stored accordingly (future PR).

The loading status is kept in room state now, as this made resolving
the race condition easier. One consequence is that the status needs
to be shared across cloned instances of RoomState. When resetting
the timeline (and cloning the room state) while lazy loading is in
progress, one of the RoomStates could be left in progress indefinitely.
Though that is more for clarity than avoiding any actual bugs.
2018-08-15 11:59:39 +02:00
Bruno Windels df758b31b7 fix lint 2018-08-15 11:59:39 +02:00
Bruno Windels 9f08bfaa6f room lazy loading tests + fix 2018-08-15 11:59:39 +02:00
Bruno Windels 198d2c780d test that modifications to clone'd() room state dont affect the old 2018-08-15 11:59:39 +02:00
Bruno Windels ab1c0dabae make sure LL members don't needlessly get disambiguated during a clone
when cloning the state, lazy loaded members are copied over with their rawDisplayName,
which could originate from their userId if they don't have a displayname.
the displayname algorithm would assume that the displayname is explicitly set,
and see if we'd have to disambiguate. As a fix, if the display name is the same as the id, just return the id
2018-08-15 11:59:39 +02:00
Bruno Windels 0234f11914 some tests for room member + state, and some fixes to make them pass 2018-08-15 11:59:39 +02:00
Bruno Windels 79fcc9f343 only set the lazy members on the forward looking state of the live timeline
since back-paginating will also support lazy loading the state needed to display that part of the timeline, and no user interaction
is supposed to happen before the lazy loaded member are, well, loaded, applying the ll members to all timelines should not be neccessary.
2018-08-15 11:59:39 +02:00
Bruno Windels 031f722540 clarify that we only get joined members for now 2018-08-15 11:59:39 +02:00
Bruno Windels 531ccf1819 actually, comment should be 1 line higher 2018-08-15 11:59:39 +02:00
Bruno Windels 0d2ac42dc4 add comment to clarify corners cut in prototype 2018-08-15 11:59:39 +02:00
Bruno Windels 9ec6ea3bdf 2, not 3 times 2018-08-15 11:59:39 +02:00
Bruno Windels 1ce580bba3 test lazy loaded info is returned and then discarded when setting a state event 2018-08-15 11:59:39 +02:00
Bruno Windels 8ad2a94a90 make sure LL member doesn't override state event
extra safety check, as this should already not happen because of the check in RoomState
2018-08-15 11:59:39 +02:00
Bruno Windels de3f75bc83 Lazy loaded members should never take precendence over members acquired through state events 2018-08-15 11:59:39 +02:00
Bruno Windels 008d85ed32 pick joined property out of response 2018-08-15 11:59:39 +02:00
Bruno Windels 5e30aff418 more consistent naming 2018-08-15 11:59:39 +02:00
Bruno Windels 5de0d39553 move the fact that we're prototyping only with joined members up the stack to client
only MatrixClient really needs to know that for now we only load joined members, the rest of the code can be generic for other membership types as that is the eventual plan, to also support invites at least.
2018-08-15 11:59:39 +02:00
Bruno Windels d95d44dc94 move error handling to caller 2018-08-15 11:59:39 +02:00
Bruno Windels 6061deac37 use method for getting state event, less code 2018-08-15 11:59:39 +02:00
Bruno Windels ba34a766e7 fix lint 2018-08-15 11:59:39 +02:00
Bruno Windels 1c81a17298 Fix tests
getSentinelMember now does return a member (with just the userid) when there is no corresponding member yet.
With lazy loading it's perfectly possible the member is not available, and null breaks continuation in the timeline.
2018-08-15 11:59:39 +02:00
Bruno Windels 2097b31d4f handle failed /joined_members call by logging to console and reverting flag 2018-08-15 11:59:39 +02:00
Bruno Windels 2155dd0552 improve return type comment 2018-08-15 11:59:39 +02:00
Bruno Windels 8733654094 remove left-over test code, oops 2018-08-15 11:59:39 +02:00
Bruno Windels abd15748ce fix one lint warning too many 2018-08-15 11:59:39 +02:00
Bruno Windels 9a796f1383 fix lint errors 2018-08-15 11:59:39 +02:00
Bruno Windels 88f2f62945 make resetting the live timeline work with lazily loaded members
In order for the lazy loading logic not to bleed into all corners
of the JS SDK, I moved some of the state copying between timelines
over to the RoomState and EventTimeLine class.
2018-08-15 11:59:39 +02:00
Bruno Windels 30adefed07 return sentinels with userid if members haven't been loaded yet,
better than braking timeline continuation
2018-08-15 11:59:39 +02:00
Bruno Windels 20a1828fa5 make sentinels lazy loading compatible
dont just rely on member events, but just copy the member
2018-08-15 11:59:39 +02:00
Bruno Windels 809674ca2b set lazily loaded members on all RoomStates of a room
for all timelines in all timeline sets
2018-08-15 11:59:39 +02:00
Bruno Windels 0ca3475878 make method to get mxc avatar url public
so MemberInfo can use it and take lazy loading into account
2018-08-15 11:59:39 +02:00
Bruno Windels 32b741e205 use more consistent naming 2018-08-15 11:59:39 +02:00
Bruno Windels c917c4a468 return correct invite sender in case of a join 2018-08-15 11:59:39 +02:00
Bruno Windels 6c584d2b4c keep is_direct checks inside RoomMember as events.member might not be available 2018-08-15 11:59:39 +02:00
Bruno Windels 759d415d40 preserve member state event if available when lazy loading members 2018-08-15 11:59:39 +02:00
Bruno Windels 45d86fa270 emit individual events for lazily loaded members
emit individual RoomState.members/newMember events
for each lazily loaded member as batch events are not a thing.
This makes updating the memberlist work
2018-08-15 11:59:39 +02:00
Bruno Windels 2c5cad71ee prototype support for lazily loading members in matrixclient 2018-08-15 11:59:39 +02:00
Bruno Windels 2b5925b893 Support for updating members in RoomState from lazily loaded members 2018-08-15 11:59:39 +02:00
Bruno Windels f012ada2c4 add setter on RoomMember to update from lazily loaded member 2018-08-15 11:59:07 +02:00
Bruno Windels af1b26ae95 Merge pull request #689 from matrix-org/dbkr/show_room_version
Add getVersion to Room
2018-08-14 17:54:09 +02:00
David Baker f72f5b43e1 Add getVersion to Room
To get the version number of the room as per https://github.com/matrix-org/matrix-doc/issues/1425
2018-08-14 14:27:08 +01:00
Hubert Chathi d55618921b initial implementation of e2e key backup and restore 2018-08-07 23:10:55 -04:00
Bruno Windels c7e1e07262 Merge pull request #680 from matrix-org/dbkr/getsyncstatedata
Add getSyncStateData()
2018-08-06 10:51:21 +02:00
Bruno Windels 24a1bec23d Merge pull request #679 from matrix-org/dbkr/chairman_mau_pt_3_sync_error
Send sync error to listener
2018-08-03 19:06:17 +02:00
David Baker 89ad104423 Add getSyncStateData()
To get additional information about the sync state (ie. the error
object).
2018-08-03 18:00:52 +01:00
David Baker c2f3324302 Send sync error to listener
We do this in other places, but not here
2018-08-03 16:59:10 +01:00
Michael Telatynski 04a969b997 Merge pull request #675 from matrix-org/bwindels/nocrashoninvalidtags
make sure room.tags is always a valid object to avoid crashes
2018-08-01 23:03:51 +01:00
Bruno Windels 630dfa9499 make sure room.tags is always a valid object so no crashes happen later on 2018-07-31 15:33:56 +02:00
Bruno Windels 95668950c2 Merge pull request #673 from matrix-org/bwindels/infinite_spinner
Fix infinite spinner upon joining a room
2018-07-31 14:57:04 +02:00
Bruno Windels 3012501e4b update docs to clarify state when emitting newMember 2018-07-30 14:19:45 +02:00
David Baker 0e81dfb004 v0.10.7 2018-07-30 11:40:08 +01:00
David Baker 35b7f358b6 Prepare changelog for v0.10.7 2018-07-30 11:40:08 +01:00
Bruno Windels e3e48944e0 add test 2018-07-27 11:49:55 +02:00
Bruno Windels 94bbba72f5 add member to members before emitting any events 2018-07-27 11:35:19 +02:00
Michael Telatynski b34716f7e9 take into account homoglyphs when calculating similar display names
to prevent homoglyph attacks

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-07-26 14:55:35 +01:00
David Baker c429ca67b9 v0.10.7-rc.1 2018-07-24 18:15:14 +01:00
David Baker bce2ba0785 Prepare changelog for v0.10.7-rc.1 2018-07-24 18:15:13 +01:00
Matthew Hodgson 2613690064 Merge pull request #666 from matrix-org/matthew/encrypt-for-invited-users
encrypt for invited users if history visibility allows.
2018-07-10 16:14:45 +01:00
David Baker 7283076bc8 Fix Users / Members mixup 2018-07-09 17:16:23 +01:00
David Baker f43d05b54e Merge branch 'master' into develop 2018-07-09 13:07:40 +01:00
David Baker c6b500bc09 v0.10.6 2018-07-09 13:06:07 +01:00
David Baker c3972015c7 Prepare changelog for v0.10.6 2018-07-09 13:06:06 +01:00
David Baker 3c18c57857 v0.10.6-rc.1 2018-07-06 15:40:55 +01:00
David Baker f562a06707 Prepare changelog for v0.10.6-rc.1 2018-07-06 15:40:54 +01:00
Michael Telatynski 7f50dd205f displayname disambiguation fixes (#662)
* fix displayname=undefined being disambiguated and strip Zero Width chars
* also strip diaritics and whitespace

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-07-06 10:42:53 +01:00
David Baker c4fe15400c Merge pull request #665 from matrix-org/luke/feature-expose-decryption-error-2
Expose event decryption error via Event.decrypted event
2018-07-05 11:02:26 +01:00
David Baker 6e3e8f7310 Merge pull request #663 from matrix-org/luke/feature-decryption-error-codes
Add decryption error codes to base.DecryptionError
2018-07-05 11:00:33 +01:00
Matthew Hodgson 5ae2c26130 lint 2018-07-05 02:01:10 +02:00
Matthew Hodgson d8d35f4022 encrypt for invited users if history visibility allows.
fixes https://github.com/vector-im/riot-web/issues/2713
2018-07-05 01:45:45 +02:00
Luke Barnard fadb4d9219 Send OLM_UNKNOWN_MESSAGE_INDEX when possible 2018-07-04 15:58:45 +01:00
Luke Barnard b63149b36a Fix error code for Olm group message decryption 2018-07-04 14:33:02 +01:00
Luke Barnard 79f92abcfa Add jsdoc for Event.decrypted error 2018-07-04 14:28:15 +01:00
Luke Barnard 0137fb468b Expose event decryption error via Event.decrypted event 2018-07-04 13:56:03 +01:00
Luke Barnard 70ef8760cc Remove spurious console.dir 2018-07-04 11:56:02 +01:00
Luke Barnard c74d2d831b Add decryption error codes to base.DecryptionError
These should roughly follow https://github.com/matrix-org/matrix-ios-sdk/blob/9732cf593206a10d2b60cd01151a759c88c1e9a2/MatrixSDK/Crypto/Algorithms/MXDecryptionResult.h#L21-L39
2018-07-04 11:54:06 +01:00
David Baker 0415f821eb v0.10.5 2018-06-29 11:32:23 +01:00
David Baker 779fe35255 Prepare changelog for v0.10.5 2018-06-29 11:32:22 +01:00
Will Hunt aec7ef6f9c getMediaLimits -> getMediaConfig 2018-06-23 14:38:39 +01:00
Will Hunt 329f09ce0a Media/limits => /config 2018-06-23 14:38:39 +01:00
Will Hunt 68c23af5ae Remove extra return 2018-06-23 14:38:39 +01:00
Will Hunt a54f30c02f Add getMediaLimits to client 2018-06-23 14:38:39 +01:00
Will Hunt fde00b1c62 getMediaLimits -> getMediaConfig 2018-06-23 12:57:29 +01:00
David Baker 11382d2cd7 v0.10.5-rc.1 2018-06-21 10:01:05 +01:00
David Baker ef31131a5d Prepare changelog for v0.10.5-rc.1 2018-06-21 10:01:05 +01:00
Will Hunt 8dd425f8ff Media/limits => /config 2018-06-20 17:24:45 +01:00
David Baker 76feabe32b Merge pull request #659 from matrix-org/t3chguy/media_fixes
fix auth header and filename=undefined
2018-06-18 13:46:59 +01:00
Michael Telatynski 7fe3e2f90a invert argument to make it positive without breaking backwards compat
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-06-18 12:27:25 +01:00
Michael Telatynski c0b2151929 allow omitting filename from upload entirely
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-06-15 18:25:20 +01:00
Michael Telatynski 5e3b1bf6b0 use Authorization header in media/v1/upload if enabled, instead of query
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-06-15 09:57:30 +01:00
Michael Telatynski fdf4523c2a export the Group model class (#656)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-06-14 13:52:13 +01:00
David Baker c6cf76f345 Merge branch 'master' into develop 2018-06-12 14:13:15 +01:00
David Baker f16e544691 v0.10.4 2018-06-12 14:11:07 +01:00
David Baker df101217fc Prepare changelog for v0.10.4 2018-06-12 14:11:07 +01:00
David Baker 5d90dc16cc Merge pull request #650 from matrix-org/t3chguy/audio_output
allow setting the output device for webrtc calls
2018-06-08 15:57:25 +01:00
David Baker bbfc1a1cd6 Merge pull request #596 from t3chguy/t3chguy/fix-docs
arguments true and false are actually invalid
2018-06-08 15:21:23 +01:00
Michael Telatynski b7208c12ac Merge pull request #643 from matrix-org/t3chguy/content-type-bugfix
fix typo where `headers` was not being used and thus sent wrong content-type
2018-06-08 12:29:35 +01:00
Michael Telatynski eaa2fdec44 Merge pull request #642 from matrix-org/t3chguy/doc_fixes
fix some documentation typos
2018-06-08 12:17:48 +01:00
David Baker 7099adfe79 v0.10.4-rc.1 2018-06-06 15:40:02 +01:00
David Baker cb17a2bcb0 Prepare changelog for v0.10.4-rc.1 2018-06-06 15:40:01 +01:00
Michael Telatynski 172044a1cb check whether notif level is undefined, because 0 is falsey (#651)
* check whether notif level is undefined, because `0` is falsey and it failed

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* improve number check for all m.room.power_levels related stuffs

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-05-29 12:11:29 +01:00
Matthew Hodgson 69204d4fb3 Merge branch 'develop' into matthew/e2e_backups 2018-05-28 00:44:49 +01:00
Michael Telatynski 4b203b6b63 allow setting the output device for webrtc calls
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-05-26 16:44:27 +01:00
David Baker 67876bab4c v0.10.3 2018-05-25 15:09:24 +01:00
David Baker 6ce691e40f Prepare changelog for v0.10.3 2018-05-25 15:09:23 +01:00
David Baker 9cd44b09f9 v0.10.3-rc.1 2018-05-24 18:16:59 +01:00
David Baker 73a2704126 Prepare changelog for v0.10.3-rc.1 2018-05-24 18:16:58 +01:00
Luke Barnard a50dd785b8 (Breaking Change) Add erase option to deactivateAccount (#649)
For erasing messages etc. after deactivation.

**Breaking change: `deactivateAccount` no longer takes callback**

Also: Move /account/deactivate from PREFIX_UNSTABLE to _R0
2018-05-24 10:47:41 +01:00
Luke Barnard bafbe5cbec Emit no_consent when M_CONSENT_NOT_GIVEN received (#647) 2018-05-22 18:02:21 +01:00
Leon 9cdcbf6bf8 emit oldEventId on "updatePendingEvent"
It should fire `Room.localEchoUpdated` event and returns the oldEventId
2018-05-22 11:27:05 +08:00
Will Hunt 9596087959 Remove extra return 2018-05-03 17:57:57 +01:00
Will Hunt 6570402b95 Add getMediaLimits to client 2018-05-03 13:43:37 +01:00
David Baker 4153845346 v0.10.2 2018-04-30 13:29:26 +01:00
David Baker 548713ed98 Prepare changelog for v0.10.2 2018-04-30 13:29:25 +01:00
Michael Telatynski 1bf1ce7070 fix typo where headers was not being used and thus sent wrong mime
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-04-30 06:56:43 +01:00
Michael Telatynski 260e7b529f fix some documentation typos
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2018-04-15 23:31:33 +01:00
Michael Telatynski b027089fc1 use constants 2018-04-15 22:16:25 +01:00
Michael Telatynski 95d582ccee arguments true and false are actually invalid 2018-01-19 23:00:34 +00:00
Matthew Hodgson e0c9b990e7 blindly move crypto.suggestKeyRestore over to /sync 2018-01-18 20:59:08 +00:00
Matthew Hodgson fb1b554b86 initial pseudocode WIP for e2e online backups 2018-01-15 01:50:24 +00:00
Johannes Bornhold 81de2b3afc Add getThirdpartyUser
Signed-off-by: Johannes Bornhold <johannes@bornhold.name>
2017-12-31 22:46:18 +01:00
Johannes Bornhold 9a53fa3876 Fix typo around getThirdpartyLocation 2017-12-31 01:24:27 +01:00
87 changed files with 14843 additions and 1093 deletions
+8 -2
View File
@@ -1,7 +1,6 @@
module.exports = {
parser: "babel-eslint",
parser: "babel-eslint", // now needed for class properties
parserOptions: {
ecmaVersion: 6,
sourceType: "module",
ecmaFeatures: {
}
@@ -67,5 +66,12 @@ module.exports = {
"padded-blocks": ["warn"],
"no-extend-native": ["warn"],
"camelcase": ["warn"],
"no-multi-spaces": ["error", { "ignoreEOLComments": true }],
"space-before-function-paren": ["error", {
"anonymous": "never",
"named": "never",
"asyncArrow": "always",
}],
"arrow-parens": "off",
}
}
+2 -1
View File
@@ -2,6 +2,7 @@
/.jsdoc
node_modules
/.npmrc
.lock-wscript
build/Release
coverage
@@ -12,6 +13,6 @@ reports
/lib
/specbuild
# version file and tarball created by 'npm pack'
# version file and tarball created by `npm pack` / `yarn pack`
/git-revision.txt
/matrix-js-sdk-*.tgz
+4 -1
View File
@@ -1,5 +1,8 @@
language: node_js
node_js:
- node # Latest stable version of nodejs.
- "10.11.0"
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.13.0
- export PATH=$HOME/.yarn/bin:$PATH
script:
- ./travis.sh
+527
View File
@@ -1,3 +1,530 @@
Changes in [1.0.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.2) (2019-03-18)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.2-rc.1...v1.0.2)
* No changes since rc.1
Changes in [1.0.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.2-rc.1) (2019-03-13)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.1...v1.0.2-rc.1)
* Use modern Yarn version on Travis CI
[\#858](https://github.com/matrix-org/matrix-js-sdk/pull/858)
* Switch to `yarn` for dependency management
[\#856](https://github.com/matrix-org/matrix-js-sdk/pull/856)
* More key request fixes
[\#855](https://github.com/matrix-org/matrix-js-sdk/pull/855)
* Calculate encrypted notification counts
[\#851](https://github.com/matrix-org/matrix-js-sdk/pull/851)
* Update dependencies
[\#854](https://github.com/matrix-org/matrix-js-sdk/pull/854)
* make sure key requests get sent
[\#850](https://github.com/matrix-org/matrix-js-sdk/pull/850)
* Use 'ideal' rather than 'exact' for deviceid
[\#852](https://github.com/matrix-org/matrix-js-sdk/pull/852)
* handle partially-shared sessions better
[\#848](https://github.com/matrix-org/matrix-js-sdk/pull/848)
Changes in [1.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.1) (2019-03-06)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.1-rc.2...v1.0.1)
* No changes since rc.2
Changes in [1.0.1-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.1-rc.2) (2019-03-05)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.1-rc.1...v1.0.1-rc.2)
* dont swallow txn errors in crypto store
[\#853](https://github.com/matrix-org/matrix-js-sdk/pull/853)
* Don't swallow txn errors in crypto store
[\#849](https://github.com/matrix-org/matrix-js-sdk/pull/849)
Changes in [1.0.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.1-rc.1) (2019-02-28)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.0...v1.0.1-rc.1)
* Fix "e is undefined" masking the original error in MegolmDecryption
[\#847](https://github.com/matrix-org/matrix-js-sdk/pull/847)
Changes in [1.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.0) (2019-02-14)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.0-rc.2...v1.0.0)
* Try again to commit package-lock.json
[\#841](https://github.com/matrix-org/matrix-js-sdk/pull/841)
Changes in [1.0.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.0-rc.2) (2019-02-14)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v1.0.0-rc.1...v1.0.0-rc.2)
* Release script: commit package-lock.json
[\#839](https://github.com/matrix-org/matrix-js-sdk/pull/839)
* Add method to force re-check of key backup
[\#840](https://github.com/matrix-org/matrix-js-sdk/pull/840)
* Fix: dont check for unverified devices in left members
[\#838](https://github.com/matrix-org/matrix-js-sdk/pull/838)
Changes in [1.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v1.0.0-rc.1) (2019-02-08)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.3...v1.0.0-rc.1)
* change hex SAS verification to decimal and emoji
[\#837](https://github.com/matrix-org/matrix-js-sdk/pull/837)
* Trust on decrypt
[\#836](https://github.com/matrix-org/matrix-js-sdk/pull/836)
* Always track our own devices
[\#835](https://github.com/matrix-org/matrix-js-sdk/pull/835)
* Make linting rules more consistent
[\#834](https://github.com/matrix-org/matrix-js-sdk/pull/834)
* add method to room to check for unverified devices
[\#833](https://github.com/matrix-org/matrix-js-sdk/pull/833)
* Merge redesign into develop
[\#831](https://github.com/matrix-org/matrix-js-sdk/pull/831)
* Supporting infrastructure for educated decisions on when to upgrade rooms
[\#830](https://github.com/matrix-org/matrix-js-sdk/pull/830)
* Include signature info for unknown devices
[\#826](https://github.com/matrix-org/matrix-js-sdk/pull/826)
* Flag v2 rooms as "safe"
[\#828](https://github.com/matrix-org/matrix-js-sdk/pull/828)
* Update ESLint
[\#821](https://github.com/matrix-org/matrix-js-sdk/pull/821)
Changes in [0.14.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.3) (2019-01-22)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.3-rc.1...v0.14.3)
* No changes since rc.1
Changes in [0.14.3-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.3-rc.1) (2019-01-17)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.2...v0.14.3-rc.1)
* Merge develop into experimental
[\#815](https://github.com/matrix-org/matrix-js-sdk/pull/815)
* Add a getAllEndToEndSessions to crypto store
[\#812](https://github.com/matrix-org/matrix-js-sdk/pull/812)
* T3chguy/fix displayname logic
[\#668](https://github.com/matrix-org/matrix-js-sdk/pull/668)
* Contributing: Note that rebase lets you mass signoff commits
[\#814](https://github.com/matrix-org/matrix-js-sdk/pull/814)
* take into account homoglyphs when calculating similar display names
[\#672](https://github.com/matrix-org/matrix-js-sdk/pull/672)
* Emit for key backup failures
[\#809](https://github.com/matrix-org/matrix-js-sdk/pull/809)
* emit oldEventId on "updatePendingEvent"
[\#646](https://github.com/matrix-org/matrix-js-sdk/pull/646)
* Add getThirdpartyUser to base api
[\#589](https://github.com/matrix-org/matrix-js-sdk/pull/589)
* Support custom status messages
[\#805](https://github.com/matrix-org/matrix-js-sdk/pull/805)
* Extra checks to avoid release script blowing up mid-process.
[\#749](https://github.com/matrix-org/matrix-js-sdk/pull/749)
* Move glob regex utilities out of the pushprocessor and into a more generic
place
[\#800](https://github.com/matrix-org/matrix-js-sdk/pull/800)
Changes in [0.14.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.2) (2018-12-10)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.2-rc.1...v0.14.2)
* No changes since rc.1
Changes in [0.14.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.2-rc.1) (2018-12-06)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.1...v0.14.2-rc.1)
* fix some assertions in e2e backup unit test
[\#794](https://github.com/matrix-org/matrix-js-sdk/pull/794)
* Config should be called with auth
[\#798](https://github.com/matrix-org/matrix-js-sdk/pull/798)
* Don't re-establish sessions with unknown devices
[\#792](https://github.com/matrix-org/matrix-js-sdk/pull/792)
* e2e key backups
[\#684](https://github.com/matrix-org/matrix-js-sdk/pull/684)
* WIP: online incremental megolm backups
[\#595](https://github.com/matrix-org/matrix-js-sdk/pull/595)
* Support for e2e key backups
[\#736](https://github.com/matrix-org/matrix-js-sdk/pull/736)
* Passphrase Support for e2e backups
[\#786](https://github.com/matrix-org/matrix-js-sdk/pull/786)
* Add 'getSsoLoginUrl' function
[\#783](https://github.com/matrix-org/matrix-js-sdk/pull/783)
* Fix: don't set the room name to null when heroes are missing.
[\#784](https://github.com/matrix-org/matrix-js-sdk/pull/784)
* Handle crypto db version upgrades
[\#785](https://github.com/matrix-org/matrix-js-sdk/pull/785)
* Restart broken Olm sessions
[\#780](https://github.com/matrix-org/matrix-js-sdk/pull/780)
* Use the last olm session that got a message
[\#776](https://github.com/matrix-org/matrix-js-sdk/pull/776)
Changes in [0.14.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.1) (2018-11-22)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.0...v0.14.1)
* Warning when crypto DB is too new to use.
Changes in [0.14.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.0) (2018-11-19)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.14.0-rc.1...v0.14.0)
* No changes since rc.1
Changes in [0.14.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.14.0-rc.1) (2018-11-15)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.13.1...v0.14.0-rc.1)
BREAKING CHANGE
----------------
* js-sdk now uses Olm 3.0. Apps using Olm must update to 3.0 to
continue using Olm with the js-sdk. The js-sdk will call Olm's
init() method when the client is started.
All Changes
-----------
* Prevent messages from being sent if other messages have failed to send
[\#781](https://github.com/matrix-org/matrix-js-sdk/pull/781)
* A unit test for olm
[\#777](https://github.com/matrix-org/matrix-js-sdk/pull/777)
* Set access_token and user_id after login in with username and password.
[\#778](https://github.com/matrix-org/matrix-js-sdk/pull/778)
* Add function to get currently joined rooms.
[\#779](https://github.com/matrix-org/matrix-js-sdk/pull/779)
* Remove the request-only stuff we don't need anymore
[\#775](https://github.com/matrix-org/matrix-js-sdk/pull/775)
* Manually construct query strings for browser-request instances
[\#770](https://github.com/matrix-org/matrix-js-sdk/pull/770)
* Fix: correctly check for crypto being present
[\#769](https://github.com/matrix-org/matrix-js-sdk/pull/769)
* Update babel-eslint to 8.1.1
[\#768](https://github.com/matrix-org/matrix-js-sdk/pull/768)
* Support `request` in the browser and support supplying servers to try in
joinRoom()
[\#764](https://github.com/matrix-org/matrix-js-sdk/pull/764)
* loglevel should be a normal dependency
[\#767](https://github.com/matrix-org/matrix-js-sdk/pull/767)
* Stop devicelist when client is stopped
[\#766](https://github.com/matrix-org/matrix-js-sdk/pull/766)
* Update to WebAssembly-powered Olm
[\#743](https://github.com/matrix-org/matrix-js-sdk/pull/743)
* Logging lib. Fixes #332
[\#763](https://github.com/matrix-org/matrix-js-sdk/pull/763)
* Use new stop() method on matrix-mock-request
[\#765](https://github.com/matrix-org/matrix-js-sdk/pull/765)
Changes in [0.13.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.13.1) (2018-11-14)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.13.0...v0.13.1)
* Add function to get currently joined rooms.
[\#779](https://github.com/matrix-org/matrix-js-sdk/pull/779)
Changes in [0.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.13.0) (2018-11-15)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.1...v0.13.0)
BREAKING CHANGE
----------------
* `MatrixClient::login` now sets client `access_token` and `user_id` following successful login with username and password.
Changes in [0.12.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.1) (2018-10-29)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.1-rc.1...v0.12.1)
* No changes since rc.1
Changes in [0.12.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.1-rc.1) (2018-10-24)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.0...v0.12.1-rc.1)
* Add repository type to package.json to make it valid
[\#762](https://github.com/matrix-org/matrix-js-sdk/pull/762)
* Add getMediaConfig()
[\#761](https://github.com/matrix-org/matrix-js-sdk/pull/761)
* add new examples, to be expanded into a post
[\#739](https://github.com/matrix-org/matrix-js-sdk/pull/739)
Changes in [0.12.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.0) (2018-10-16)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.12.0-rc.1...v0.12.0)
* No changes since rc.1
Changes in [0.12.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.12.0-rc.1) (2018-10-11)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.1...v0.12.0-rc.1)
BREAKING CHANGES
----------------
* If js-sdk finds data in the store that is incompatible with the options currently being used,
it will emit sync state ERROR with an error of type InvalidStoreError. It will also stop trying
to sync in this situation: the app must stop the client and then either clear the store or
change the options (in this case, enable or disable lazy loading of members) and then start
the client again.
All Changes
-----------
* never replace /sync'ed memberships with OOB ones
[\#760](https://github.com/matrix-org/matrix-js-sdk/pull/760)
* Don't fail to start up if lazy load check fails
[\#759](https://github.com/matrix-org/matrix-js-sdk/pull/759)
* Make e2e work on Edge
[\#754](https://github.com/matrix-org/matrix-js-sdk/pull/754)
* throw error with same name and message over idb worker boundary
[\#758](https://github.com/matrix-org/matrix-js-sdk/pull/758)
* Default to a room version of 1 when there is no room create event
[\#755](https://github.com/matrix-org/matrix-js-sdk/pull/755)
* Silence bluebird warnings
[\#757](https://github.com/matrix-org/matrix-js-sdk/pull/757)
* allow non-ff merge from release branch into master
[\#750](https://github.com/matrix-org/matrix-js-sdk/pull/750)
* Reject with the actual error on indexeddb error
[\#751](https://github.com/matrix-org/matrix-js-sdk/pull/751)
* Update mocha to v5
[\#744](https://github.com/matrix-org/matrix-js-sdk/pull/744)
* disable lazy loading for guests as they cant create filters
[\#748](https://github.com/matrix-org/matrix-js-sdk/pull/748)
* Revert "Add getMediaLimits to client"
[\#745](https://github.com/matrix-org/matrix-js-sdk/pull/745)
Changes in [0.11.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.1) (2018-10-01)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.1-rc.1...v0.11.1)
* No changes since rc.1
Changes in [0.11.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.1-rc.1) (2018-09-27)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.0...v0.11.1-rc.1)
* make usage of hub compatible with latest version (2.5)
[\#747](https://github.com/matrix-org/matrix-js-sdk/pull/747)
* Detect when lazy loading has been toggled in client.startClient
[\#746](https://github.com/matrix-org/matrix-js-sdk/pull/746)
* Add getMediaLimits to client
[\#644](https://github.com/matrix-org/matrix-js-sdk/pull/644)
* Split npm start into an init and watch script
[\#742](https://github.com/matrix-org/matrix-js-sdk/pull/742)
* Revert "room name should only take canonical alias into account"
[\#738](https://github.com/matrix-org/matrix-js-sdk/pull/738)
* fix display name disambiguation with LL
[\#737](https://github.com/matrix-org/matrix-js-sdk/pull/737)
* Introduce Room.myMembership event
[\#735](https://github.com/matrix-org/matrix-js-sdk/pull/735)
* room name should only take canonical alias into account
[\#733](https://github.com/matrix-org/matrix-js-sdk/pull/733)
* state events from context response were not wrapped in a MatrixEvent
[\#732](https://github.com/matrix-org/matrix-js-sdk/pull/732)
* Reduce amount of promises created when inserting members
[\#724](https://github.com/matrix-org/matrix-js-sdk/pull/724)
* dont wait for LL members to be stored to resolve the members
[\#726](https://github.com/matrix-org/matrix-js-sdk/pull/726)
* RoomState.members emitted with wrong argument order for OOB members
[\#728](https://github.com/matrix-org/matrix-js-sdk/pull/728)
Changes in [0.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (2018-09-10)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.0-rc.1...v0.11.0)
BREAKING CHANGES
----------------
* v0.11.0-rc.1 introduced some breaking changes - see the respective release notes.
No changes since rc.1
Changes in [0.11.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0-rc.1) (2018-09-07)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.9...v0.11.0-rc.1)
* Support for lazy loading members. This should improve performance for
users who joined big rooms a lot. Pass to `lazyLoadMembers = true` option when calling `startClient`.
BREAKING CHANGES
----------------
* `MatrixClient::startClient` now returns a Promise. No method should be called on the client before that promise resolves. Before this method didn't return anything.
* A new `CATCHUP` sync state, emitted by `MatrixClient#"sync"` and returned by `MatrixClient::getSyncState()`, when doing initial sync after the `ERROR` state. See `MatrixClient` documentation for details.
* `RoomState::maySendEvent('m.room.message', userId)` & `RoomState::maySendMessage(userId)` do not check the membership of the user anymore, only the power level. To check if the syncing user is allowed to write in a room, use `Room::maySendMessage()` as `RoomState` is not always aware of the syncing user's membership anymore, in case lazy loading of members is enabled.
All Changes
-----------
* Only emit CATCHUP if recovering from conn error
[\#727](https://github.com/matrix-org/matrix-js-sdk/pull/727)
* Fix docstring for sync data.error
[\#725](https://github.com/matrix-org/matrix-js-sdk/pull/725)
* Re-apply "Don't rely on members to query if syncing user can post to room"
[\#723](https://github.com/matrix-org/matrix-js-sdk/pull/723)
* Revert "Don't rely on members to query if syncing user can post to room"
[\#721](https://github.com/matrix-org/matrix-js-sdk/pull/721)
* Don't rely on members to query if syncing user can post to room
[\#717](https://github.com/matrix-org/matrix-js-sdk/pull/717)
* Fixes for room.guessDMUserId
[\#719](https://github.com/matrix-org/matrix-js-sdk/pull/719)
* Fix filepanel also filtering main timeline with LL turned on.
[\#716](https://github.com/matrix-org/matrix-js-sdk/pull/716)
* Remove lazy loaded members when leaving room
[\#711](https://github.com/matrix-org/matrix-js-sdk/pull/711)
* Fix: show spinner again while recovering from connection error
[\#702](https://github.com/matrix-org/matrix-js-sdk/pull/702)
* Add method to query LL state in client
[\#714](https://github.com/matrix-org/matrix-js-sdk/pull/714)
* Fix: also load invited members when lazy loading members
[\#707](https://github.com/matrix-org/matrix-js-sdk/pull/707)
* Pass through function to discard megolm session
[\#704](https://github.com/matrix-org/matrix-js-sdk/pull/704)
Changes in [0.10.9](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.9) (2018-09-03)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.9-rc.2...v0.10.9)
* No changes since rc.2
Changes in [0.10.9-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.9-rc.2) (2018-08-31)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.9-rc.1...v0.10.9-rc.2)
* Fix for "otherMember.getAvatarUrl is not a function"
[\#708](https://github.com/matrix-org/matrix-js-sdk/pull/708)
Changes in [0.10.9-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.9-rc.1) (2018-08-30)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.8...v0.10.9-rc.1)
* Fix DM avatar
[\#706](https://github.com/matrix-org/matrix-js-sdk/pull/706)
* Lazy loading: avoid loading members at initial sync for e2e rooms
[\#699](https://github.com/matrix-org/matrix-js-sdk/pull/699)
* Improve setRoomEncryption guard against multiple m.room.encryption st…
[\#700](https://github.com/matrix-org/matrix-js-sdk/pull/700)
* Revert "Lazy loading: don't block on setting up room crypto"
[\#698](https://github.com/matrix-org/matrix-js-sdk/pull/698)
* Lazy loading: don't block on setting up room crypto
[\#696](https://github.com/matrix-org/matrix-js-sdk/pull/696)
* Add getVisibleRooms()
[\#695](https://github.com/matrix-org/matrix-js-sdk/pull/695)
* Add wrapper around getJoinedMemberCount()
[\#697](https://github.com/matrix-org/matrix-js-sdk/pull/697)
* Api to fetch events via /room/.../event/..
[\#694](https://github.com/matrix-org/matrix-js-sdk/pull/694)
* Support for room upgrades
[\#693](https://github.com/matrix-org/matrix-js-sdk/pull/693)
* Lazy loading of room members
[\#691](https://github.com/matrix-org/matrix-js-sdk/pull/691)
Changes in [0.10.8](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.8) (2018-08-20)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.8-rc.1...v0.10.8)
* No changes since rc.1
Changes in [0.10.8-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.8-rc.1) (2018-08-16)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.7...v0.10.8-rc.1)
* Add getVersion to Room
[\#689](https://github.com/matrix-org/matrix-js-sdk/pull/689)
* Add getSyncStateData()
[\#680](https://github.com/matrix-org/matrix-js-sdk/pull/680)
* Send sync error to listener
[\#679](https://github.com/matrix-org/matrix-js-sdk/pull/679)
* make sure room.tags is always a valid object to avoid crashes
[\#675](https://github.com/matrix-org/matrix-js-sdk/pull/675)
* Fix infinite spinner upon joining a room
[\#673](https://github.com/matrix-org/matrix-js-sdk/pull/673)
Changes in [0.10.7](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.7) (2018-07-30)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.7-rc.1...v0.10.7)
* No changes since rc.1
Changes in [0.10.7-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.7-rc.1) (2018-07-24)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.6...v0.10.7-rc.1)
* encrypt for invited users if history visibility allows.
[\#666](https://github.com/matrix-org/matrix-js-sdk/pull/666)
Changes in [0.10.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.6) (2018-07-09)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.6-rc.1...v0.10.6)
* No changes since rc.1
Changes in [0.10.6-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.6-rc.1) (2018-07-06)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.5...v0.10.6-rc.1)
* Expose event decryption error via Event.decrypted event
[\#665](https://github.com/matrix-org/matrix-js-sdk/pull/665)
* Add decryption error codes to base.DecryptionError
[\#663](https://github.com/matrix-org/matrix-js-sdk/pull/663)
Changes in [0.10.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.5) (2018-06-29)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.5-rc.1...v0.10.5)
* No changes since rc.1
Changes in [0.10.5-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.5-rc.1) (2018-06-21)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.4...v0.10.5-rc.1)
* fix auth header and filename=undefined
[\#659](https://github.com/matrix-org/matrix-js-sdk/pull/659)
* allow setting the output device for webrtc calls
[\#650](https://github.com/matrix-org/matrix-js-sdk/pull/650)
* arguments true and false are actually invalid
[\#596](https://github.com/matrix-org/matrix-js-sdk/pull/596)
* fix typo where `headers` was not being used and thus sent wrong content-type
[\#643](https://github.com/matrix-org/matrix-js-sdk/pull/643)
* fix some documentation typos
[\#642](https://github.com/matrix-org/matrix-js-sdk/pull/642)
Changes in [0.10.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.4) (2018-06-12)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.4-rc.1...v0.10.4)
* No changes since rc.1
Changes in [0.10.4-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.4-rc.1) (2018-06-06)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.3...v0.10.4-rc.1)
* check whether notif level is undefined, because `0` is falsey
[\#651](https://github.com/matrix-org/matrix-js-sdk/pull/651)
Changes in [0.10.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.3) (2018-05-25)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.3-rc.1...v0.10.3)
* No changes since v0.10.3-rc.1
Changes in [0.10.3-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.3-rc.1) (2018-05-24)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.2...v0.10.3-rc.1)
BREAKING CHANGE
---------------
The deprecated 'callback' parameter has been removed from MatrixBaseApis.deactivateAccount
* Add `erase` option to deactivateAccount
[\#649](https://github.com/matrix-org/matrix-js-sdk/pull/649)
* Emit Session.no_consent when M_CONSENT_NOT_GIVEN received
[\#647](https://github.com/matrix-org/matrix-js-sdk/pull/647)
Changes in [0.10.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.2) (2018-04-30)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.2-rc.1...v0.10.2)
* No changes from rc.1
Changes in [0.10.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.2-rc.1) (2018-04-25)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.1...v0.10.2-rc.1)
+5
View File
@@ -113,3 +113,8 @@ include the line in your commit or pull request comment::
can't be accepted. Git makes this trivial - just use the -s flag when you do
``git commit``, having first set ``user.name`` and ``user.email`` git configs
(which you should have done anyway :)
If you forgot to sign off your commits before making your pull request and are on git 2.17+
you can mass signoff using rebase::
git rebase --signoff origin/develop
+68 -12
View File
@@ -21,7 +21,11 @@ Please check [the working browser example](examples/browser) for more informatio
In Node.js
----------
``npm install matrix-js-sdk``
Ensure you have the latest LTS version of Node.js installed.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://yarnpkg.com/docs/install/) if you do not have it already.
``yarn add matrix-js-sdk``
```javascript
var sdk = require("matrix-js-sdk");
@@ -30,9 +34,61 @@ In Node.js
console.log("Public Rooms: %s", JSON.stringify(data));
});
```
See below for how to include libolm to enable end-to-end-encryption. Please check
[the Node.js terminal app](examples/node) for a more complex example.
To start the client:
```javascript
await client.startClient({initialSyncLimit: 10});
```
You can perform a call to `/sync` to get the current state of the client:
```javascript
client.once('sync', function(state, prevState, res) {
if(state === 'PREPARED') {
console.log("prepared");
} else {
console.log(state);
process.exit(1);
}
});
```
To send a message:
```javascript
var content = {
"body": "message text",
"msgtype": "m.text"
};
client.sendEvent("roomId", "m.room.message", content, "", (err, res) => {
console.log(err);
});
```
To listen for message events:
```javascript
client.on("Room.timeline", function(event, room, toStartOfTimeline) {
if (event.getType() !== "m.room.message") {
return; // only use messages
}
console.log(event.event.content.body);
});
```
By default, the `matrix-js-sdk` client uses the `MatrixInMemoryStore` to store events as they are received. For example to iterate through the currently stored timeline for a room:
```javascript
Object.keys(client.store.rooms).forEach((roomId) => {
client.getRoom(roomId).timeline.forEach(t => {
console.log(t.event);
});
});
```
What does this SDK do?
----------------------
@@ -231,7 +287,7 @@ This SDK uses JSDoc3 style comments. You can manually build and
host the API reference from the source files like this:
```
$ npm run gendoc
$ yarn gendoc
$ cd .jsdoc
$ python -m SimpleHTTPServer 8005
```
@@ -267,15 +323,15 @@ To provide the Olm library in a browser application:
To provide the Olm library in a node.js application:
* ``npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz``
* ``yarn add https://matrix.org/packages/npm/olm/olm-3.0.0.tgz``
(replace the URL with the latest version you want to use from
https://matrix.org/packages/npm/olm/)
* ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``.
If you want to package Olm as dependency for your node.js application, you
can use ``npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz
--save-optional`` (if your application also works without e2e crypto enabled)
or ``--save`` (if it doesn't) to do so.
If you want to package Olm as dependency for your node.js application, you can
use ``yarn add https://matrix.org/packages/npm/olm/olm-3.0.0.tgz``. If your
application also works without e2e crypto enabled, add ``--optional`` to mark it
as an optional dependency.
Contributing
@@ -285,7 +341,7 @@ want to use this SDK, skip this section.*
First, you need to pull in the right build tools:
```
$ npm install
$ yarn install
```
Building
@@ -293,20 +349,20 @@ Building
To build a browser version from scratch when developing::
```
$ npm run build
$ yarn build
```
To constantly do builds when files are modified (using ``watchify``)::
```
$ npm run watch
$ yarn watch
```
To run tests (Jasmine)::
```
$ npm test
$ yarn test
```
To run linting:
```
$ npm run lint
$ yarn lint
```
+13 -1
View File
@@ -1,5 +1,17 @@
var matrixcs = require("./lib/matrix");
matrixcs.request(require("browser-request"));
const request = require('browser-request');
const queryString = require('qs');
matrixcs.request(function(opts, fn) {
// We manually fix the query string for browser-request because
// it doesn't correctly handle cases like ?via=one&via=two. Instead
// we mimic `request`'s query string interface to make it all work
// as expected.
// browser-request will happily take the constructed string as the
// query string without trying to modify it further.
opts.qs = queryString.stringify(opts.qs || {}, opts.qsStringifyOptions);
return request(opts, fn);
});
// just *accessing* indexedDB throws an exception in firefox with
// indexeddb disabled.
+3 -3
View File
@@ -202,9 +202,9 @@ function printRoomList() {
dateStr = new Date(msg.getTs()).toISOString().replace(
/T/, ' ').replace(/\..+/, '');
}
var me = roomList[i].getMember(myUserId);
if (me) {
fmt = fmts[me.membership];
var myMembership = roomList[i].getMyMembership();
if (myMembership) {
fmt = fmts[myMembership];
}
var roomName = fixWidth(roomList[i].name, 25);
print(
+1 -1
View File
@@ -21,4 +21,4 @@ export PATH="$rootdir/node_modules/.bin:$PATH"
# now run our checks
cd "$tmpdir"
npm run lint
yarn lint
+9 -7
View File
@@ -5,8 +5,8 @@ set -x
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 6 || exit $?
npm install || exit $?
nvm use 10 || exit $?
yarn install || exit $?
RC=0
@@ -18,17 +18,19 @@ function fail {
# don't use last time's test reports
rm -rf reports coverage || exit $?
npm test || fail "npm test finished with return code $?"
yarn test || fail "yarn test finished with return code $?"
npm run -s lint -- -f checkstyle > eslint.xml ||
yarn -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 $?"
# `yarn pack` doesn't seem to run scripts, however that seems okay here as we
# just built as part of `install` above.
yarn pack ||
fail "yarn pack finished with return code $?"
npm run gendoc || fail "JSDoc failed with code $?"
yarn gendoc || fail "JSDoc failed with code $?"
exit $RC
+24 -15
View File
@@ -1,24 +1,27 @@
{
"name": "matrix-js-sdk",
"version": "0.10.2-rc.1",
"version": "1.0.2",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"test:build": "babel -s -d specbuild spec",
"test:run": "istanbul cover --report text --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" node_modules/mocha/bin/_mocha -- --recursive specbuild --colors --reporter mocha-jenkins-reporter --reporter-options junit_report_path=reports/test-results.xml",
"test:watch": "mocha --watch --compilers js:babel-core/register --recursive spec --colors",
"test": "npm run test:build && npm run test:run",
"check": "npm run test:build && _mocha --recursive specbuild --colors",
"test": "yarn test:build && yarn test:run",
"check": "yarn test:build && _mocha --recursive specbuild --colors",
"gendoc": "babel --no-babelrc -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
"start": "babel -s -w -d lib src",
"start": "yarn start:init && yarn start:watch",
"start:watch": "babel -s -w --skip-initial-build -d lib src",
"start:init": "babel -s -d lib src",
"clean": "rimraf lib dist",
"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",
"dist": "yarn build",
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
"lint": "eslint --max-warnings 102 src spec",
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt"
"lint": "eslint --max-warnings 101 src spec",
"prepare": "yarn clean && yarn build && git rev-parse HEAD > git-revision.txt"
},
"repository": {
"type": "git",
"url": "https://github.com/matrix-org/matrix-js-sdk"
},
"keywords": [
@@ -51,34 +54,40 @@
"dependencies": {
"another-json": "^0.2.0",
"babel-runtime": "^6.26.0",
"base-x": "3.0.4",
"bluebird": "^3.5.0",
"browser-request": "^0.3.3",
"bs58": "^4.0.1",
"content-type": "^1.0.2",
"request": "^2.53.0"
"loglevel": "1.6.1",
"qs": "^6.5.2",
"request": "^2.88.0",
"unhomoglyph": "^1.0.2"
},
"devDependencies": {
"babel-cli": "^6.18.0",
"babel-eslint": "^7.1.1",
"babel-eslint": "^10.0.1",
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2015": "^6.18.0",
"browserify": "^14.0.0",
"browserify": "^16.2.3",
"browserify-shim": "^3.8.13",
"eslint": "^3.13.1",
"eslint": "^5.12.0",
"eslint-config-google": "^0.7.1",
"exorcist": "^0.4.0",
"expect": "^1.20.2",
"istanbul": "^0.4.5",
"jsdoc": "^3.5.5",
"lolex": "^1.5.2",
"matrix-mock-request": "^1.2.0",
"mocha": "^3.2.0",
"mocha-jenkins-reporter": "^0.3.6",
"matrix-mock-request": "^1.2.2",
"mocha": "^5.2.0",
"mocha-jenkins-reporter": "^0.4.0",
"olm": "https://matrix.org/packages/npm/olm/olm-3.1.0-pre1.tgz",
"rimraf": "^2.5.4",
"source-map-support": "^0.4.11",
"sourceify": "^0.1.0",
"uglify-js": "^2.8.26",
"watchify": "^3.2.1"
"watchify": "^3.11.1"
},
"browserify": {
"transform": [
+50 -15
View File
@@ -6,12 +6,26 @@
# 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
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
# npm; typically installed by Node.js
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
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 $$)
if [[ `command -v hub` ]] && [[ `hub --version` =~ hub[[:space:]]version[[:space:]]([0-9]*).([0-9]*) ]]; then
HUB_VERSION_MAJOR=${BASH_REMATCH[1]}
HUB_VERSION_MINOR=${BASH_REMATCH[2]}
if [[ $HUB_VERSION_MAJOR -lt 2 ]] || [[ $HUB_VERSION_MAJOR -eq 2 && $HUB_VERSION_MINOR -lt 5 ]]; then
echo "hub version 2.5 is required, you have $HUB_VERSION_MAJOR.$HUB_VERSION_MINOR installed"
exit
fi
else
echo "hub is required: please install it"
exit
fi
npm --version > /dev/null || (echo "npm is required: please install it"; kill $$)
yarn --version > /dev/null || (echo "yarn is required: please install it"; kill $$)
USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z"
@@ -45,7 +59,8 @@ fi
skip_changelog=
skip_jsdoc=
changelog_file="CHANGELOG.md"
while getopts hc:xz f; do
expected_npm_user="matrixdotorg"
while getopts hc:u:xz f; do
case $f in
h)
help
@@ -60,6 +75,9 @@ while getopts hc:xz f; do
z)
skip_jsdoc=1
;;
u)
expected_npm_user="$OPTARG"
;;
esac
done
shift `expr $OPTIND - 1`
@@ -74,6 +92,14 @@ if [ -z "$skip_changelog" ]; then
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
fi
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
actual_npm_user=`npm whoami`;
if [ $expected_npm_user != $actual_npm_user ]; then
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
exit 1
fi
# ignore leading v on release
release="${1#v}"
tag="v${release}"
@@ -127,14 +153,22 @@ cat "${changelog_file}" | `dirname $0`/scripts/changelog_head.py > "${latest_cha
set -x
# Bump package.json and build the dist
echo "npm version"
# npm version will automatically commit its modification
echo "yarn version"
# yarn 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"
yarn version --no-git-tag-version --new-version "$release"
# commit yarn.lock if it exists, is versioned, and is modified
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]];
then
pkglock='yarn.lock'
else
pkglock=''
fi
git commit package.json $pkglock -m "$tag"
# figure out if we should be signing this release
@@ -150,7 +184,7 @@ fi
# 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)
# (rather than whatever we're pulling in from yarn link)
assets=''
dodist=0
jq -e .scripts.dist package.json 2> /dev/null || dodist=$?
@@ -161,10 +195,10 @@ if [ $dodist -eq 0 ]; then
pushd "$builddir"
git clone "$projdir" .
git checkout "$rel_branch"
npm install
yarn install
# We haven't tagged yet, so tell the dist script what version
# it's building
DIST_VERSION="$tag" npm run dist
DIST_VERSION="$tag" yarn dist
popd
@@ -245,7 +279,7 @@ release_text=`mktemp`
echo "$tag" > "${release_text}"
echo >> "${release_text}"
cat "${latest_changes}" >> "${release_text}"
hub release create $hubflags $assets -f "${release_text}" "$tag"
hub release create $hubflags $assets -F "${release_text}" "$tag"
if [ $dodist -eq 0 ]; then
rm -rf "$builddir"
@@ -253,12 +287,13 @@ fi
rm "${release_text}"
rm "${latest_changes}"
# publish to npmjs
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
npm publish
if [ -z "$skip_jsdoc" ]; then
echo "generating jsdocs"
npm run gendoc
yarn gendoc
echo "copying jsdocs to gh-pages branch"
git checkout gh-pages
@@ -281,9 +316,9 @@ fi
echo "updating master branch"
git checkout master
git pull
git merge --ff-only "$rel_branch"
git merge "$rel_branch"
# push master and docs (if generated) to github
# push master and docs (if generated) to github
git push origin master
if [ -z "$skip_jsdoc" ]; then
git push origin gh-pages
+19 -9
View File
@@ -1,7 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2018-2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -38,9 +38,10 @@ import LocalStorageCryptoStore from '../lib/crypto/store/localStorage-crypto-sto
*
* @param {WebStorage=} sessionStoreBackend a web storage object to use for the
* session store. If undefined, we will create a MockStorageApi.
* @param {object} options additional options to pass to the client
*/
export default function TestClient(
userId, deviceId, accessToken, sessionStoreBackend,
userId, deviceId, accessToken, sessionStoreBackend, options,
) {
this.userId = userId;
this.deviceId = deviceId;
@@ -50,19 +51,22 @@ export default function TestClient(
}
const sessionStore = new sdk.WebStorageSessionStore(sessionStoreBackend);
// expose this so the tests can get to it
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
this.httpBackend = new MockHttpBackend();
this.client = sdk.createClient({
options = Object.assign({
baseUrl: "http://" + userId + ".test.server",
userId: userId,
accessToken: accessToken,
deviceId: deviceId,
sessionStore: sessionStore,
cryptoStore: this.cryptoStore,
request: this.httpBackend.requestFn,
});
}, options);
if (!options.cryptoStore) {
// expose this so the tests can get to it
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
options.cryptoStore = this.cryptoStore;
}
this.client = sdk.createClient(options);
this.deviceKeys = null;
this.oneTimeKeys = {};
@@ -102,9 +106,11 @@ TestClient.prototype.start = function() {
/**
* stop the client
* @return {Promise} Resolves once the mock http backend has finished all pending flushes
*/
TestClient.prototype.stop = function() {
this.client.stopClient();
return this.httpBackend.stop();
};
/**
@@ -179,7 +185,11 @@ TestClient.prototype.expectKeyQuery = function(response) {
this.httpBackend.when('POST', '/keys/query').respond(
200, (path, content) => {
Object.keys(response.device_keys).forEach((userId) => {
expect(content.device_keys[userId]).toEqual({});
expect(content.device_keys[userId]).toEqual(
{},
"Expected key query for " + userId + ", got " +
Object.keys(content.device_keys),
);
});
return response;
});
+3 -1
View File
@@ -97,10 +97,11 @@ describe("DeviceList management:", function() {
});
afterEach(function() {
aliceTestClient.stop();
return aliceTestClient.stop();
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']);
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
@@ -143,6 +144,7 @@ describe("DeviceList management:", function() {
it("We should not get confused by out-of-order device query responses",
() => {
// https://github.com/vector-im/riot-web/issues/3126
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
+21 -6
View File
@@ -72,7 +72,11 @@ function expectAliQueryKeys() {
bobKeys[bobDeviceId] = bobTestClient.deviceKeys;
aliTestClient.httpBackend.when("POST", "/keys/query")
.respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual({});
expect(content.device_keys[bobUserId]).toEqual(
{},
"Expected Alice to key query for " + bobUserId + ", got " +
Object.keys(content.device_keys),
);
const result = {};
result[bobUserId] = bobKeys;
return {device_keys: result};
@@ -96,7 +100,11 @@ function expectBobQueryKeys() {
bobTestClient.httpBackend.when(
"POST", "/keys/query",
).respond(200, function(path, content) {
expect(content.device_keys[aliUserId]).toEqual({});
expect(content.device_keys[aliUserId]).toEqual(
{},
"Expected Bob to key query for " + aliUserId + ", got " +
Object.keys(content.device_keys),
);
const result = {};
result[aliUserId] = aliKeys;
return {device_keys: result};
@@ -410,10 +418,10 @@ describe("MatrixClient crypto", function() {
});
afterEach(function() {
aliTestClient.stop();
aliTestClient.httpBackend.verifyNoOutstandingExpectation();
bobTestClient.stop();
bobTestClient.httpBackend.verifyNoOutstandingExpectation();
return Promise.all([aliTestClient.stop(), bobTestClient.stop()]);
});
it("Bob uploads device keys", function() {
@@ -544,6 +552,7 @@ describe("MatrixClient crypto", function() {
});
it("Ali sends a message", function(done) {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -554,6 +563,7 @@ describe("MatrixClient crypto", function() {
});
it("Bob receives a message", function() {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -564,6 +574,7 @@ describe("MatrixClient crypto", function() {
});
it("Bob receives a message with a bogus sender", function() {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -617,6 +628,7 @@ describe("MatrixClient crypto", function() {
});
it("Ali blocks Bob's device", function(done) {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -636,6 +648,7 @@ describe("MatrixClient crypto", function() {
});
it("Bob receives two pre-key messages", function(done) {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -649,6 +662,8 @@ describe("MatrixClient crypto", function() {
});
it("Bob replies to the message", function() {
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
bobTestClient.expectKeyQuery({device_keys: {[bobUserId]: {}}});
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => bobTestClient.start())
@@ -659,13 +674,14 @@ describe("MatrixClient crypto", function() {
.then(bobRecvMessage)
.then(bobEnablesEncryption)
.then(bobSendsReplyMessage).then(function(ciphertext) {
expect(ciphertext.type).toEqual(1);
expect(ciphertext.type).toEqual(1, "Unexpected cipghertext type.");
}).then(aliRecvMessage);
});
it("Ali does a key query when encryption is enabled", function() {
// enabling encryption in the room should make alice download devices
// for both members.
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
return Promise.resolve()
.then(() => aliTestClient.start())
.then(() => firstSync(aliTestClient))
@@ -696,7 +712,6 @@ describe("MatrixClient crypto", function() {
}).then(() => {
aliTestClient.expectKeyQuery({
device_keys: {
[aliUserId]: {},
[bobUserId]: {},
},
});
@@ -30,6 +30,7 @@ describe("MatrixClient events", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
return httpBackend.stop();
});
describe("emissions", function() {
@@ -111,6 +111,7 @@ describe("getEventTimeline support", function() {
if (client) {
client.stopClient();
}
return httpBackend.stop();
});
it("timeline support must be enabled to work", function(done) {
+2 -1
View File
@@ -41,6 +41,7 @@ describe("MatrixClient", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
return httpBackend.stop();
});
describe("uploadContent", function() {
@@ -159,7 +160,7 @@ describe("MatrixClient", function() {
describe("joinRoom", function() {
it("should no-op if you've already joined a room", function() {
const roomId = "!foo:bar";
const room = new Room(roomId);
const room = new Room(roomId, userId);
room.addLiveEvents([
utils.mkMembership({
user: userId, room: roomId, mship: "join", event: true,
+12 -15
View File
@@ -64,6 +64,7 @@ describe("MatrixClient opts", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
return httpBackend.stop();
});
describe("without opts.store", function() {
@@ -94,7 +95,7 @@ describe("MatrixClient opts", function() {
httpBackend.flush("/txn1", 1);
});
it("should be able to sync / get new events", function(done) {
it("should be able to sync / get new events", async function() {
const expectedEventTypes = [ // from /initialSync
"m.room.message", "m.room.name", "m.room.member", "m.room.member",
"m.room.create",
@@ -110,20 +111,16 @@ describe("MatrixClient opts", function() {
httpBackend.when("GET", "/pushrules").respond(200, {});
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("/filter", 1);
}).then(function() {
return Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]);
}).done(function() {
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes,
);
done();
});
await client.startClient();
await httpBackend.flush("/pushrules", 1);
await httpBackend.flush("/filter", 1);
await Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client),
]);
expect(expectedEventTypes.length).toEqual(
0, "Expected to see event types: " + expectedEventTypes,
);
});
});
@@ -36,6 +36,7 @@ describe("MatrixClient retrying", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
return httpBackend.stop();
});
xit("should retry according to MatrixScheduler.retryFn", function() {
@@ -130,6 +130,7 @@ describe("MatrixClient room timelines", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
return httpBackend.stop();
});
describe("local echo events", function() {
+1
View File
@@ -38,6 +38,7 @@ describe("MatrixClient syncing", function() {
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
client.stopClient();
return httpBackend.stop();
});
describe("startClient", function() {
+13 -2
View File
@@ -296,7 +296,7 @@ describe("megolm", function() {
});
afterEach(function() {
aliceTestClient.stop();
return aliceTestClient.stop();
});
it("Alice receives a megolm message", function() {
@@ -499,6 +499,7 @@ describe("megolm", function() {
it('Alice sends a megolm message', function() {
let p2pSession;
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
@@ -581,6 +582,7 @@ describe("megolm", function() {
});
it("We shouldn't attempt to send to blocked devices", function() {
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
@@ -634,6 +636,7 @@ describe("megolm", function() {
let p2pSession;
let megolmSessionId;
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
@@ -817,8 +820,14 @@ describe("megolm", function() {
};
});
// Grab the event that we'll need to resend
const room = aliceTestClient.client.getRoom(ROOM_ID);
const pendingEvents = room.getPendingEvents();
expect(pendingEvents.length).toEqual(1);
const unsentEvent = pendingEvents[0];
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.client.resendEvent(unsentEvent, room),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
@@ -837,6 +846,7 @@ describe("megolm", function() {
let downloadPromise;
let sendPromise;
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
@@ -880,6 +890,7 @@ describe("megolm", function() {
it("Alice exports megolm keys and imports them to a new device", function() {
let messageEncrypted;
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
return aliceTestClient.start().then(() => {
// establish an olm session with alice
return createOlmSession(testOlmAccount, aliceTestClient);
+670
View File
@@ -0,0 +1,670 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import 'source-map-support/register';
import Promise from 'bluebird';
const sdk = require("../..");
const utils = require("../test-utils");
const AutoDiscovery = sdk.AutoDiscovery;
import expect from 'expect';
import MockHttpBackend from "matrix-mock-request";
describe("AutoDiscovery", function() {
let httpBackend = null;
beforeEach(function() {
utils.beforeEach(this); // eslint-disable-line no-invalid-this
httpBackend = new MockHttpBackend();
sdk.request(httpBackend.requestFn);
});
it("should throw an error when no domain is specified", function() {
return Promise.all([
AutoDiscovery.findClientConfig(/* no args */).then(() => {
throw new Error("Expected a failure, not success with no args");
}, () => {
return true;
}),
AutoDiscovery.findClientConfig("").then(() => {
throw new Error("Expected a failure, not success with an empty string");
}, () => {
return true;
}),
AutoDiscovery.findClientConfig(null).then(() => {
throw new Error("Expected a failure, not success with null");
}, () => {
return true;
}),
AutoDiscovery.findClientConfig(true).then(() => {
throw new Error("Expected a failure, not success with a non-string");
}, () => {
return true;
}),
]);
});
it("should return PROMPT when .well-known 404s", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(404, {});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "PROMPT",
error: null,
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known returns a 500 error", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(500, {});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known returns a 400 error", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(400, {});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known returns an empty body", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "");
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known returns not-JSON", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, "abc");
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
"m.homeserver (empty string)", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_PROMPT when .well-known does not have a base_url for " +
"m.homeserver (no property)", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_PROMPT",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (disallowed scheme)", function() {
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "mxc://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (verification failure: 404)", function() {
httpBackend.when("GET", "/_matrix/client/versions").respond(404, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (verification failure: 500)", function() {
httpBackend.when("GET", "/_matrix/client/versions").respond(500, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when .well-known has an invalid base_url for " +
"m.homeserver (verification failure: 200 but wrong content)", function() {
httpBackend.when("GET", "/_matrix/client/versions").respond(200, {
not_matrix_versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return SUCCESS when .well-known has a verifiably accurate base_url for " +
"m.homeserver", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri).toEqual("https://example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
base_url: "https://example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
base_url: "https://example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return SUCCESS with the right homeserver URL", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "PROMPT",
error: null,
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when the identity server configuration is wrong " +
"(missing base_url)", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
not_base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when the identity server configuration is wrong " +
"(empty base_url)", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when the identity server configuration is wrong " +
"(validation error: 404)", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(404, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return FAIL_ERROR when the identity server configuration is wrong " +
"(validation error: 500)", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(500, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
// We still expect the base_url to be here for debugging purposes
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
error: "Invalid identity server discovery response",
base_url: null,
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return SUCCESS when the identity server configuration is " +
"verifiably accurate", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
expect(req.opts.uri)
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
}).respond(200, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "https://identity.example.org",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "SUCCESS",
error: null,
base_url: "https://identity.example.org",
},
};
expect(conf).toEqual(expected);
}),
]);
});
it("should return SUCCESS and preserve non-standard keys from the " +
".well-known response", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
}).respond(200, {
versions: ["r0.0.1"],
});
httpBackend.when("GET", "/_matrix/identity/api/v1").check((req) => {
expect(req.opts.uri)
.toEqual("https://identity.example.org/_matrix/identity/api/v1");
}).respond(200, {});
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
"m.homeserver": {
// Note: we also expect this test to trim the trailing slash
base_url: "https://chat.example.org/",
},
"m.identity_server": {
base_url: "https://identity.example.org",
},
"org.example.custom.property": {
cupcakes: "yes",
},
});
return Promise.all([
httpBackend.flushAllExpected(),
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "SUCCESS",
error: null,
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "SUCCESS",
error: null,
base_url: "https://identity.example.org",
},
"org.example.custom.property": {
cupcakes: "yes",
},
};
expect(conf).toEqual(expected);
}),
]);
});
});
+354 -8
View File
@@ -1,20 +1,366 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
let Crypto;
if (sdk.CRYPTO_ENABLED) {
Crypto = require("../../lib/crypto");
}
import '../olm-loader';
import Crypto from '../../lib/crypto';
import expect from 'expect';
import WebStorageSessionStore from '../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../MockStorageApi';
import TestClient from '../TestClient';
import {MatrixEvent} from '../../lib/models/event';
import Room from '../../lib/models/room';
import olmlib from '../../lib/crypto/olmlib';
import lolex from 'lolex';
const EventEmitter = require("events").EventEmitter;
const sdk = require("../..");
const Olm = global.Olm;
describe("Crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
return;
}
beforeEach(function(done) {
Olm.init().then(done);
});
it("Crypto exposes the correct olm library version", function() {
expect(Crypto.getOlmVersion()[0]).toEqual(2);
expect(Crypto.getOlmVersion()[0]).toEqual(3);
});
describe('Session management', function() {
const otkResponse = {
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
'signed_curve25519:FLIBBLE': {
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
signatures: {
'@alice:home.server': {
'ed25519:aliceDevice': 'totally a valid signature',
},
},
},
},
},
},
};
let crypto;
let mockBaseApis;
let mockRoomList;
let fakeEmitter;
beforeEach(async function() {
const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage);
const cryptoStore = new MemoryCryptoStore(mockStorage);
cryptoStore.storeEndToEndDeviceData({
devices: {
'@bob:home.server': {
'BOBDEVICE': {
keys: {
'curve25519:BOBDEVICE': 'this is a key',
},
},
},
},
trackingStatus: {},
});
mockBaseApis = {
sendToDevice: expect.createSpy(),
getKeyBackupVersion: expect.createSpy(),
isGuest: expect.createSpy(),
};
mockRoomList = {};
fakeEmitter = new EventEmitter();
crypto = new Crypto(
mockBaseApis,
sessionStore,
"@alice:home.server",
"FLIBBLE",
sessionStore,
cryptoStore,
mockRoomList,
);
crypto.registerEventHandlers(fakeEmitter);
await crypto.init();
});
afterEach(async function() {
await crypto.stop();
});
it("restarts wedged Olm sessions", async function() {
const prom = new Promise((resolve) => {
mockBaseApis.claimOneTimeKeys = function() {
resolve();
return otkResponse;
};
});
fakeEmitter.emit('toDeviceEvent', {
getType: expect.createSpy().andReturn('m.room.message'),
getContent: expect.createSpy().andReturn({
msgtype: 'm.bad.encrypted',
}),
getWireContent: expect.createSpy().andReturn({
algorithm: 'm.olm.v1.curve25519-aes-sha2',
sender_key: 'this is a key',
}),
getSender: expect.createSpy().andReturn('@bob:home.server'),
});
await prom;
});
});
describe('Key requests', function() {
let aliceClient;
let bobClient;
beforeEach(async function() {
aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
bobClient = (new TestClient(
"@bob:example.com", "bobdevice",
)).client;
await aliceClient.initCrypto();
await bobClient.initCrypto();
});
afterEach(async function() {
aliceClient.stopClient();
bobClient.stopClient();
});
it(
"does not cancel keyshare requests if some messages are not decrypted",
async function() {
function awaitEvent(emitter, event) {
return new Promise((resolve, reject) => {
emitter.once(event, (result) => {
resolve(result);
});
});
}
async function keyshareEventForEvent(event, index) {
const eventContent = event.getWireContent();
const key = await aliceClient._crypto._olmDevice
.getInboundGroupSessionKey(
roomId, eventContent.sender_key, eventContent.session_id,
index,
);
const ksEvent = new MatrixEvent({
type: "m.forwarded_room_key",
sender: "@alice:example.com",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: roomId,
sender_key: eventContent.sender_key,
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
session_id: eventContent.session_id,
session_key: key.key,
chain_index: key.chain_index,
forwarding_curve25519_key_chain:
key.forwarding_curve_key_chain,
},
});
// make onRoomKeyEvent think this was an encrypted event
ksEvent._senderCurve25519Key = "akey";
return ksEvent;
}
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
const events = [
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$1",
content: {
msgtype: "m.text",
body: "1",
},
}),
new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$2",
content: {
msgtype: "m.text",
body: "2",
},
}),
];
await Promise.all(events.map(async (event) => {
// alice encrypts each event, and then bob tries to decrypt
// them without any keys, so that they'll be in pending
await aliceClient._crypto.encryptEvent(event, aliceRoom);
event._clearEvent = {};
event._senderCurve25519Key = null;
event._claimedEd25519Key = null;
try {
await bobClient._crypto.decryptEvent(event);
} catch (e) {
// we expect this to fail because we don't have the
// decryption keys yet
}
}));
const bobDecryptor = bobClient._crypto._getRoomDecryptor(
roomId, olmlib.MEGOLM_ALGORITHM,
);
let eventPromise = Promise.all(events.map((ev) => {
return awaitEvent(ev, "Event.decrypted");
}));
// keyshare the session key starting at the second message, so
// the first message can't be decrypted yet, but the second one
// can
let ksEvent = await keyshareEventForEvent(events[1], 1);
await bobDecryptor.onRoomKeyEvent(ksEvent);
await eventPromise;
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
expect(events[1].getContent().msgtype).toNotBe("m.bad.encrypted");
const cryptoStore = bobClient._cryptoStore;
const eventContent = events[0].getWireContent();
const senderKey = eventContent.sender_key;
const sessionId = eventContent.session_id;
const roomKeyRequestBody = {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: roomId,
sender_key: senderKey,
session_id: sessionId,
};
// the room key request should still be there, since we haven't
// decrypted everything
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
.toExist();
// keyshare the session key starting at the first message, so
// that it can now be decrypted
eventPromise = awaitEvent(events[0], "Event.decrypted");
ksEvent = await keyshareEventForEvent(events[0], 0);
await bobDecryptor.onRoomKeyEvent(ksEvent);
await eventPromise;
expect(events[0].getContent().msgtype).toNotBe("m.bad.encrypted");
// the room key request should be gone since we've now decypted everything
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
.toNotExist();
},
);
it("creates a new keyshare request if we request a keyshare", async function() {
// make sure that cancelAndResend... creates a new keyshare request
// if there wasn't an already-existing one
const event = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
session_id: "sessionid",
sender_key: "senderkey",
},
});
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
const cryptoStore = aliceClient._cryptoStore;
const roomKeyRequestBody = {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: "!someroom",
session_id: "sessionid",
sender_key: "senderkey",
};
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
.toExist();
});
it("uses a new txnid for re-requesting keys", async function() {
const event = new MatrixEvent({
sender: "@bob:example.com",
room_id: "!someroom",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
session_id: "sessionid",
sender_key: "senderkey",
},
});
/* return a promise and a function. When the function is called,
* the promise will be resolved.
*/
function awaitFunctionCall() {
let func;
const promise = new Promise((resolve, reject) => {
func = function(...args) {
resolve(args);
return new Promise((resolve, reject) => {
// give us some time to process the result before
// continuing
global.setTimeout(resolve, 1);
});
};
});
return {func, promise};
}
aliceClient.startClient();
const clock = lolex.install();
try {
let promise;
// make a room key request, and record the transaction ID for the
// sendToDevice call
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
clock.runToLast();
let args = await promise;
const txnId = args[2];
clock.runToLast();
// give the room key request manager time to update the state
// of the request
await Promise.resolve();
// cancel and resend the room key request
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
clock.runToLast();
// the first call to sendToDevice will be the cancellation
args = await promise;
// the second call to sendToDevice will be the key request
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
clock.runToLast();
args = await promise;
clock.runToLast();
expect(args[2]).toNotBe(txnId);
} finally {
clock.uninstall();
}
});
});
});
+12 -1
View File
@@ -59,16 +59,25 @@ describe('DeviceList', function() {
let downloadSpy;
let sessionStore;
let cryptoStore;
let deviceLists = [];
beforeEach(function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
deviceLists = [];
downloadSpy = expect.createSpy();
const mockStorage = new MockStorageApi();
sessionStore = new WebStorageSessionStore(mockStorage);
cryptoStore = new MemoryCryptoStore();
});
afterEach(function() {
for (const dl of deviceLists) {
dl.stop();
}
});
function createTestDeviceList() {
const baseApis = {
downloadKeysForUsers: downloadSpy,
@@ -76,7 +85,9 @@ describe('DeviceList', function() {
const mockOlm = {
verifySignature: function(key, message, signature) {},
};
return new DeviceList(baseApis, cryptoStore, sessionStore, mockOlm);
const dl = new DeviceList(baseApis, cryptoStore, sessionStore, mockOlm);
deviceLists.push(dl);
return dl;
}
it("should successfully download and store device keys", function() {
+98 -16
View File
@@ -1,8 +1,4 @@
try {
global.Olm = require('olm');
} catch (e) {
console.warn("unable to run megolm tests: libolm not available");
}
import '../../../olm-loader';
import expect from 'expect';
import Promise from 'bluebird';
@@ -13,20 +9,17 @@ import WebStorageSessionStore from '../../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../../MockStorageApi';
import testUtils from '../../../test-utils';
// Crypto and OlmDevice won't import unless we have global.Olm
let OlmDevice;
let Crypto;
if (global.Olm) {
OlmDevice = require('../../../../lib/crypto/OlmDevice');
Crypto = require('../../../../lib/crypto');
}
import OlmDevice from '../../../../lib/crypto/OlmDevice';
import Crypto from '../../../../lib/crypto';
const MatrixEvent = sdk.MatrixEvent;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const ROOM_ID = '!ROOM:ID';
const Olm = global.Olm;
describe("MegolmDecryption", function() {
if (!global.Olm) {
console.warn('Not running megolm unit tests: libolm not present');
@@ -38,9 +31,11 @@ describe("MegolmDecryption", function() {
let mockCrypto;
let mockBaseApis;
beforeEach(function() {
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
await Olm.init();
mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockBaseApis = {};
@@ -69,7 +64,7 @@ describe("MegolmDecryption", function() {
describe('receives some keys:', function() {
let groupSession;
beforeEach(function() {
beforeEach(async function() {
groupSession = new global.Olm.OutboundGroupSession();
groupSession.create();
@@ -98,7 +93,7 @@ describe("MegolmDecryption", function() {
},
};
return event.attemptDecryption(mockCrypto).then(() => {
await event.attemptDecryption(mockCrypto).then(() => {
megolmDecryption.onRoomKeyEvent(event);
});
});
@@ -266,5 +261,92 @@ describe("MegolmDecryption", function() {
// test is successful if no exception is thrown
});
});
it("re-uses sessions for sequential messages", async function() {
const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage);
const cryptoStore = new MemoryCryptoStore(mockStorage);
const olmDevice = new OlmDevice(sessionStore, cryptoStore);
olmDevice.verifySignature = expect.createSpy();
await olmDevice.init();
mockBaseApis.claimOneTimeKeys = expect.createSpy().andReturn(Promise.resolve({
one_time_keys: {
'@alice:home.server': {
aliceDevice: {
'signed_curve25519:flooble': {
key: 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI',
signatures: {
'@alice:home.server': {
'ed25519:aliceDevice': 'totally valid',
},
},
},
},
},
},
}));
mockBaseApis.sendToDevice = expect.createSpy().andReturn(Promise.resolve());
mockCrypto.downloadKeys.andReturn(Promise.resolve({
'@alice:home.server': {
aliceDevice: {
deviceId: 'aliceDevice',
isBlocked: expect.createSpy().andReturn(false),
isUnverified: expect.createSpy().andReturn(false),
getIdentityKey: expect.createSpy().andReturn(
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
),
getFingerprint: expect.createSpy().andReturn(''),
},
},
}));
const megolmEncryption = new MegolmEncryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
config: {
rotation_period_ms: 9999999999999,
},
});
const mockRoom = {
getEncryptionTargetMembers: expect.createSpy().andReturn(
[{userId: "@alice:home.server"}],
),
getBlacklistUnverifiedDevices: expect.createSpy().andReturn(false),
};
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some text",
});
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
// this should have claimed a key for alice as it's starting a new session
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
);
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
['@alice:home.server'], false,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
);
mockBaseApis.claimOneTimeKeys.reset();
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
body: "Some more text",
});
// this should *not* have claimed a key as it should be using the same session
expect(mockBaseApis.claimOneTimeKeys).toNotHaveBeenCalled();
// likewise they should show the same session ID
expect(ct2.session_id).toEqual(ct1.session_id);
});
});
});
+86
View File
@@ -0,0 +1,86 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import '../../../olm-loader';
import expect from 'expect';
import WebStorageSessionStore from '../../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../../MockStorageApi';
import testUtils from '../../../test-utils';
import OlmDevice from '../../../../lib/crypto/OlmDevice';
function makeOlmDevice() {
const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage);
const cryptoStore = new MemoryCryptoStore(mockStorage);
const olmDevice = new OlmDevice(sessionStore, cryptoStore);
return olmDevice;
}
async function setupSession(initiator, opponent) {
await opponent.generateOneTimeKeys(1);
const keys = await opponent.getOneTimeKeys();
const firstKey = Object.values(keys['curve25519'])[0];
const sid = await initiator.createOutboundSession(
opponent.deviceCurve25519Key, firstKey,
);
return sid;
}
describe("OlmDecryption", function() {
if (!global.Olm) {
console.warn('Not running megolm unit tests: libolm not present');
return;
}
let aliceOlmDevice;
let bobOlmDevice;
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
await global.Olm.init();
aliceOlmDevice = makeOlmDevice();
bobOlmDevice = makeOlmDevice();
await aliceOlmDevice.init();
await bobOlmDevice.init();
});
describe('olm', function() {
it("can decrypt messages", async function() {
const sid = await setupSession(aliceOlmDevice, bobOlmDevice);
const ciphertext = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key,
sid,
"The olm or proteus is an aquatic salamander in the family Proteidae",
);
const result = await bobOlmDevice.createInboundSession(
aliceOlmDevice.deviceCurve25519Key,
ciphertext.type,
ciphertext.body,
);
expect(result.payload).toEqual(
"The olm or proteus is an aquatic salamander in the family Proteidae",
);
});
});
});
+471
View File
@@ -0,0 +1,471 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import '../../olm-loader';
import expect from 'expect';
import Promise from 'bluebird';
import sdk from '../../..';
import algorithms from '../../../lib/crypto/algorithms';
import WebStorageSessionStore from '../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../MockStorageApi';
import testUtils from '../../test-utils';
import OlmDevice from '../../../lib/crypto/OlmDevice';
import Crypto from '../../../lib/crypto';
const Olm = global.Olm;
const MatrixClient = sdk.MatrixClient;
const MatrixEvent = sdk.MatrixEvent;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const ROOM_ID = '!ROOM:ID';
const SESSION_ID = 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc';
const ENCRYPTED_EVENT = new MatrixEvent({
type: 'm.room.encrypted',
room_id: '!ROOM:ID',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
sender_key: 'SENDER_CURVE25519',
session_id: SESSION_ID,
ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N'
+ 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl'
+ 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs',
},
event_id: '$event1',
origin_server_ts: 1507753886000,
});
const KEY_BACKUP_DATA = {
first_message_index: 0,
forwarded_count: 0,
is_verified: false,
session_data: {
ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw'
+ '6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZ'
+ 'Slne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9'
+ 'SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOy'
+ 'Syw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGF'
+ 'ru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV'
+ '4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxv'
+ 'C+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpe'
+ 'Ug5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3Nf'
+ 'QHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPy'
+ 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg',
mac: '5lxYBHQU80M',
ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14',
},
};
const BACKUP_INFO = {
algorithm: "m.megolm_backup.v1",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
};
function makeTestClient(sessionStore, cryptoStore) {
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
const store = [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
return new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
request: function() {}, // NOP
store: store,
scheduler: scheduler,
userId: "@alice:bar",
deviceId: "device",
sessionStore: sessionStore,
cryptoStore: cryptoStore,
});
}
describe("MegolmBackup", function() {
if (!global.Olm) {
console.warn('Not running megolm backup unit tests: libolm not present');
return;
}
let olmDevice;
let mockOlmLib;
let mockCrypto;
let mockStorage;
let sessionStore;
let cryptoStore;
let megolmDecryption;
beforeEach(async function() {
await Olm.init();
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
mockCrypto = testUtils.mock(Crypto, 'Crypto');
mockCrypto.backupKey = new Olm.PkEncryption();
mockCrypto.backupKey.set_recipient_key(
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
);
mockCrypto.backupInfo = BACKUP_INFO;
mockStorage = new MockStorageApi();
sessionStore = new WebStorageSessionStore(mockStorage);
cryptoStore = new MemoryCryptoStore(mockStorage);
olmDevice = new OlmDevice(sessionStore, cryptoStore);
// we stub out the olm encryption bits
mockOlmLib = {};
mockOlmLib.ensureOlmSessionsForDevices = expect.createSpy();
mockOlmLib.encryptMessageForDevice =
expect.createSpy().andReturn(Promise.resolve());
});
describe("backup", function() {
let mockBaseApis;
let realSetTimeout;
beforeEach(function() {
mockBaseApis = {};
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: mockBaseApis,
roomId: ROOM_ID,
});
megolmDecryption.olmlib = mockOlmLib;
// clobber the setTimeout function to run 100x faster.
// ideally we would use lolex, but we have no oportunity
// to tick the clock between the first try and the retry.
realSetTimeout = global.setTimeout;
global.setTimeout = function(f, n) {
return realSetTimeout(f, n/100);
};
});
afterEach(function() {
global.setTimeout = realSetTimeout;
});
it('automatically calls the key back up', function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
// construct a fake decrypted key event via the use of a mocked
// 'crypto' implementation.
const event = new MatrixEvent({
type: 'm.room.encrypted',
});
const decryptedData = {
clearEvent: {
type: 'm.room_key',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
room_id: ROOM_ID,
session_id: groupSession.session_id(),
session_key: groupSession.session_key(),
},
},
senderCurve25519Key: "SENDER_CURVE25519",
claimedEd25519Key: "SENDER_ED25519",
};
mockCrypto.decryptEvent = function() {
return Promise.resolve(decryptedData);
};
mockCrypto.cancelRoomKeyRequest = function() {};
mockCrypto.backupGroupSession = expect.createSpy();
return event.attemptDecryption(mockCrypto).then(() => {
return megolmDecryption.onRoomKeyEvent(event);
}).then(() => {
expect(mockCrypto.backupGroupSession).toHaveBeenCalled();
});
});
it('sends backups to the server', function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());
const client = makeTestClient(sessionStore, cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto()
.then(() => {
return cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
},
txn);
});
})
.then(() => {
client.enableKeyBackup({
algorithm: "m.megolm_backup.v1",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client._http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqualTo(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe(1);
expect(data.rooms[ROOM_ID].sessions).toExist();
expect(data.rooms[ROOM_ID].sessions).toIncludeKey(
groupSession.session_id(),
);
resolve();
return Promise.resolve({});
};
client._crypto.backupGroupSession(
"roomId",
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
[],
groupSession.session_id(),
groupSession.session_key(),
);
}).then(() => {
expect(numCalls).toBe(1);
});
});
});
it('retries when a backup fails', function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
"setProcessFunction",
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
const store = [
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
const client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: "https://identity.server",
accessToken: "my.access.token",
request: function() {}, // NOP
store: store,
scheduler: scheduler,
userId: "@alice:bar",
deviceId: "device",
sessionStore: sessionStore,
cryptoStore: cryptoStore,
});
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto()
.then(() => {
return cryptoStore.doTxn(
"readwrite",
[cryptoStore.STORE_SESSION],
(txn) => {
cryptoStore.addEndToEndInboundGroupSession(
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
groupSession.session_id(),
{
forwardingCurve25519KeyChain: undefined,
keysClaimed: {
ed25519: "SENDER_ED25519",
},
room_id: ROOM_ID,
session: ibGroupSession.pickle(olmDevice._pickleKey),
},
txn);
});
})
.then(() => {
client.enableKeyBackup({
algorithm: "foobar",
version: 1,
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
});
let numCalls = 0;
return new Promise((resolve, reject) => {
client._http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqualTo(2);
if (numCalls >= 3) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({});
}
expect(method).toBe("PUT");
expect(path).toBe("/room_keys/keys");
expect(queryParams.version).toBe(1);
expect(data.rooms[ROOM_ID].sessions).toExist();
expect(data.rooms[ROOM_ID].sessions).toIncludeKey(
groupSession.session_id(),
);
if (numCalls > 1) {
resolve();
return Promise.resolve({});
} else {
return Promise.reject(
new Error("this is an expected failure"),
);
}
};
client._crypto.backupGroupSession(
"roomId",
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
[],
groupSession.session_id(),
groupSession.session_key(),
);
}).then(() => {
expect(numCalls).toBe(2);
});
});
});
});
describe("restore", function() {
let client;
beforeEach(function() {
client = makeTestClient(sessionStore, cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
megolmDecryption.olmlib = mockOlmLib;
return client.initCrypto();
});
afterEach(function() {
client.stopClient();
});
it('can restore from backup', function() {
client._http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA);
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
});
});
it('can restore backup by room', function() {
client._http.authedRequest = function() {
return Promise.resolve({
rooms: {
[ROOM_ID]: {
sessions: {
[SESSION_ID]: KEY_BACKUP_DATA,
},
},
},
});
};
return client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
null, null, BACKUP_INFO,
).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => {
expect(res.clearEvent.content).toEqual('testytest');
});
});
});
});
@@ -0,0 +1,146 @@
/*
Copyright 2018-2019 New Vector 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.
*/
try {
global.Olm = require('olm');
} catch (e) {
console.warn("unable to run device verification tests: libolm not available");
}
import expect from 'expect';
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
import {ShowQRCode, ScanQRCode} from '../../../../lib/crypto/verification/QRCode';
const Olm = global.Olm;
describe("QR code verification", function() {
if (!global.Olm) {
console.warn('Not running device verification tests: libolm not present');
return;
}
beforeEach(async function() {
await Olm.init();
});
describe("showing", function() {
it("should emit an event to show a QR code", async function() {
const qrCode = new ShowQRCode({
getUserId: () => "@alice:example.com",
deviceId: "ABCDEFG",
getDeviceEd25519Key: function() {
return "device+ed25519+key";
},
});
const spy = expect.createSpy().andCall((e) => {
qrCode.done();
});
qrCode.on("show_qr_code", spy);
await qrCode.verify();
expect(spy).toHaveBeenCalledWith({
url: "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey",
});
});
});
describe("scanning", function() {
const QR_CODE_URL = "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey";
it("should verify when a QR code is sent", async function() {
const device = DeviceInfo.fromStorage(
{
algorithms: [],
keys: {
"curve25519:ABCDEFG": "device+curve25519+key",
"ed25519:ABCDEFG": "device+ed25519+key",
},
verified: false,
known: false,
unsigned: {},
},
"ABCDEFG",
);
const client = {
getStoredDevice: expect.createSpy().andReturn(device),
setDeviceVerified: expect.createSpy(),
};
const qrCode = new ScanQRCode(client);
qrCode.on("confirm_user_id", ({userId, confirm}) => {
if (userId === "@alice:example.com") {
confirm();
} else {
qrCode.cancel(new Error("Incorrect user"));
}
});
qrCode.on("scan", ({done}) => {
done(QR_CODE_URL);
});
await qrCode.verify();
expect(client.getStoredDevice)
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
expect(client.setDeviceVerified)
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
});
it("should error when the user ID doesn't match", async function() {
const client = {
getStoredDevice: expect.createSpy(),
setDeviceVerified: expect.createSpy(),
};
const qrCode = new ScanQRCode(client, "@bob:example.com", "ABCDEFG");
qrCode.on("scan", ({done}) => {
done(QR_CODE_URL);
});
const spy = expect.createSpy();
await qrCode.verify().catch(spy);
expect(spy).toHaveBeenCalled();
expect(client.getStoredDevice).toNotHaveBeenCalled();
expect(client.setDeviceVerified).toNotHaveBeenCalled();
});
it("should error if the key doesn't match", async function() {
const device = DeviceInfo.fromStorage(
{
algorithms: [],
keys: {
"curve25519:ABCDEFG": "device+curve25519+key",
"ed25519:ABCDEFG": "a+different+device+ed25519+key",
},
verified: false,
known: false,
unsigned: {},
},
"ABCDEFG",
);
const client = {
getStoredDevice: expect.createSpy().andReturn(device),
setDeviceVerified: expect.createSpy(),
};
const qrCode = new ScanQRCode(client, "@alice:example.com", "ABCDEFG");
qrCode.on("scan", ({done}) => {
done(QR_CODE_URL);
});
const spy = expect.createSpy();
await qrCode.verify().catch(spy);
expect(spy).toHaveBeenCalled();
expect(client.getStoredDevice).toHaveBeenCalled();
expect(client.setDeviceVerified).toNotHaveBeenCalled();
});
});
});
@@ -0,0 +1,75 @@
/*
Copyright 2019 New Vector 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.
*/
try {
global.Olm = require('olm');
} catch (e) {
console.warn("unable to run device verification tests: libolm not available");
}
import expect from 'expect';
import {verificationMethods} from '../../../../lib/crypto';
import SAS from '../../../../lib/crypto/verification/SAS';
const Olm = global.Olm;
import {makeTestClients} from './util';
describe("verification request", function() {
if (!global.Olm) {
console.warn('Not running device verification unit tests: libolm not present');
return;
}
beforeEach(async function() {
await Olm.init();
});
it("should request and accept a verification", async function() {
const [alice, bob] = await makeTestClients(
[
{userId: "@alice:example.com", deviceId: "Osborne2"},
{userId: "@bob:example.com", deviceId: "Dynabook"},
],
{
verificationMethods: [verificationMethods.SAS],
},
);
alice._crypto._deviceList.getRawStoredDevicesForUser = function() {
return {
Dynabook: {
keys: {
"ed25519:Dynabook": "bob+base64+ed25519+key",
},
},
};
};
alice.downloadKeys = () => {
return Promise.resolve();
};
bob.downloadKeys = () => {
return Promise.resolve();
};
bob.on("crypto.verification.request", (request) => {
const bobVerifier = request.beginKeyVerification(verificationMethods.SAS);
bobVerifier.verify();
});
const aliceVerifier = await alice.requestVerification("@bob:example.com");
expect(aliceVerifier).toBeAn(SAS);
});
});
+207
View File
@@ -0,0 +1,207 @@
/*
Copyright 2018-2019 New Vector 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.
*/
try {
global.Olm = require('olm');
} catch (e) {
console.warn("unable to run device verification tests: libolm not available");
}
import expect from 'expect';
import sdk from '../../../..';
import {verificationMethods} from '../../../../lib/crypto';
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
import SAS from '../../../../lib/crypto/verification/SAS';
const Olm = global.Olm;
const MatrixEvent = sdk.MatrixEvent;
import {makeTestClients} from './util';
describe("SAS verification", function() {
if (!global.Olm) {
console.warn('Not running device verification unit tests: libolm not present');
return;
}
beforeEach(async function() {
await Olm.init();
});
it("should error on an unexpected event", async function() {
const sas = new SAS({}, "@alice:example.com", "ABCDEFG");
sas.handleEvent(new MatrixEvent({
sender: "@alice:example.com",
type: "es.inquisition",
content: {},
}));
const spy = expect.createSpy();
await sas.verify()
.catch(spy);
expect(spy).toHaveBeenCalled();
});
it("should verify a key", async function() {
const [alice, bob] = await makeTestClients(
[
{userId: "@alice:example.com", deviceId: "Osborne2"},
{userId: "@bob:example.com", deviceId: "Dynabook"},
],
{
verificationMethods: [verificationMethods.SAS],
},
);
alice.setDeviceVerified = expect.createSpy();
alice.getDeviceEd25519Key = () => {
return "alice+base64+ed25519+key";
};
alice.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
keys: {
"ed25519:Dynabook": "bob+base64+ed25519+key",
},
},
"Dynabook",
);
};
alice.downloadKeys = () => {
return Promise.resolve();
};
bob.setDeviceVerified = expect.createSpy();
bob.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
keys: {
"ed25519:Osborne2": "alice+base64+ed25519+key",
},
},
"Osborne2",
);
};
bob.getDeviceEd25519Key = () => {
return "bob+base64+ed25519+key";
};
bob.downloadKeys = () => {
return Promise.resolve();
};
let aliceSasEvent;
let bobSasEvent;
const bobPromise = new Promise((resolve, reject) => {
bob.on("crypto.verification.start", (verifier) => {
verifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
bobSasEvent = e;
} else {
try {
expect(e.sas).toEqual(aliceSasEvent.sas);
e.confirm();
aliceSasEvent.confirm();
} catch (error) {
e.mismatch();
aliceSasEvent.mismatch();
}
}
});
resolve(verifier);
});
});
const aliceVerifier = alice.beginKeyVerification(
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
);
aliceVerifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!bobSasEvent) {
aliceSasEvent = e;
} else {
try {
expect(e.sas).toEqual(bobSasEvent.sas);
e.confirm();
bobSasEvent.confirm();
} catch (error) {
e.mismatch();
bobSasEvent.mismatch();
}
}
});
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
]);
expect(alice.setDeviceVerified)
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
expect(bob.setDeviceVerified)
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
});
it("should send a cancellation message on error", async function() {
const [alice, bob] = await makeTestClients(
[
{userId: "@alice:example.com", deviceId: "Osborne2"},
{userId: "@bob:example.com", deviceId: "Dynabook"},
],
{
verificationMethods: [verificationMethods.SAS],
},
);
alice.setDeviceVerified = expect.createSpy();
alice.downloadKeys = () => {
return Promise.resolve();
};
bob.setDeviceVerified = expect.createSpy();
bob.downloadKeys = () => {
return Promise.resolve();
};
const bobPromise = new Promise((resolve, reject) => {
bob.on("crypto.verification.start", (verifier) => {
verifier.on("show_sas", (e) => {
e.mismatch();
});
resolve(verifier);
});
});
const aliceVerifier = alice.beginKeyVerification(
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
);
const aliceSpy = expect.createSpy();
const bobSpy = expect.createSpy();
await Promise.all([
aliceVerifier.verify().catch(aliceSpy),
bobPromise.then((verifier) => verifier.verify()).catch(bobSpy),
]);
expect(aliceSpy).toHaveBeenCalled();
expect(bobSpy).toHaveBeenCalled();
expect(alice.setDeviceVerified)
.toNotHaveBeenCalled();
expect(bob.setDeviceVerified)
.toNotHaveBeenCalled();
});
});
+63
View File
@@ -0,0 +1,63 @@
/*
Copyright 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import TestClient from '../../../TestClient';
import sdk from '../../../..';
const MatrixEvent = sdk.MatrixEvent;
export async function makeTestClients(userInfos, options) {
const clients = [];
const clientMap = {};
const sendToDevice = function(type, map) {
// console.log(this.getUserId(), "sends", type, map);
for (const [userId, devMap] of Object.entries(map)) {
if (userId in clientMap) {
for (const [deviceId, msg] of Object.entries(devMap)) {
if (deviceId in clientMap[userId]) {
const event = new MatrixEvent({
sender: this.getUserId(), // eslint-disable-line no-invalid-this
type: type,
content: msg,
});
setTimeout(
() => clientMap[userId][deviceId]
.emit("toDeviceEvent", event),
0,
);
}
}
}
}
};
for (const userInfo of userInfos) {
const client = (new TestClient(
userInfo.userId, userInfo.deviceId, undefined, undefined,
options,
)).client;
if (!(userInfo.userId in clientMap)) {
clientMap[userInfo.userId] = {};
}
clientMap[userInfo.userId][userInfo.deviceId] = client;
client.sendToDevice = sendToDevice;
clients.push(client);
}
await Promise.all(clients.map((client) => client.initCrypto()));
return clients;
}
+31 -21
View File
@@ -139,6 +139,9 @@ describe("MatrixClient", function() {
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
store.getClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
store.storeClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
store.isNewlyCreated = expect.createSpy().andReturn(Promise.resolve(true));
client = new MatrixClient({
baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl,
@@ -182,7 +185,7 @@ describe("MatrixClient", function() {
});
});
it("should not POST /filter if a matching filter already exists", function(done) {
it("should not POST /filter if a matching filter already exists", async function() {
httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(SYNC_RESPONSE);
@@ -191,15 +194,19 @@ describe("MatrixClient", function() {
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();
}
const syncPromise = new Promise((resolve, reject) => {
client.on("sync", function syncListener(state) {
if (state === "SYNCING") {
expect(httpLookups.length).toEqual(0);
client.removeListener("sync", syncListener);
resolve();
} else if (state === "ERROR") {
reject(new Error("sync error"));
}
});
});
await client.startClient();
await syncPromise;
});
describe("getSyncState", function() {
@@ -207,15 +214,18 @@ describe("MatrixClient", function() {
expect(client.getSyncState()).toBe(null);
});
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();
}
it("should return the same sync state as emitted sync events", async function() {
const syncingPromise = new Promise((resolve) => {
client.on("sync", function syncListener(state) {
expect(state).toEqual(client.getSyncState());
if (state === "SYNCING") {
client.removeListener("sync", syncListener);
resolve();
}
});
});
client.startClient();
await client.startClient();
await syncingPromise;
});
});
@@ -258,8 +268,8 @@ describe("MatrixClient", function() {
});
describe("retryImmediately", function() {
it("should return false if there is no request waiting", function() {
client.startClient();
it("should return false if there is no request waiting", async function() {
await client.startClient();
expect(client.retryImmediately()).toBe(false);
});
@@ -380,7 +390,7 @@ describe("MatrixClient", function() {
client.startClient();
});
it("should transition ERROR -> PREPARED after /sync if prev failed",
it("should transition ERROR -> CATCHUP after /sync if prev failed",
function(done) {
const expectedStates = [];
acceptKeepalives = false;
@@ -403,7 +413,7 @@ describe("MatrixClient", function() {
expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["PREPARED", "ERROR"]);
expectedStates.push(["CATCHUP", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done));
client.startClient();
});
+56
View File
@@ -192,6 +192,15 @@ describe("RoomMember", function() {
});
});
describe("isOutOfBand", function() {
it("should be set by markOutOfBand", function() {
const member = new RoomMember();
expect(member.isOutOfBand()).toEqual(false);
member.markOutOfBand();
expect(member.isOutOfBand()).toEqual(true);
});
});
describe("setMembershipEvent", function() {
const joinEvent = utils.mkMembership({
event: true,
@@ -276,5 +285,52 @@ describe("RoomMember", function() {
member.setMembershipEvent(joinEvent); // no-op
expect(emitCount).toEqual(1);
});
it("should set 'name' to user_id if it is just whitespace", function() {
const joinEvent = utils.mkMembership({
event: true,
mship: "join",
user: userA,
room: roomId,
name: " \u200b ",
});
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent);
expect(member.name).toEqual(userA); // it should fallback because all whitespace
});
it("should disambiguate users on a fuzzy displayname match", function() {
const joinEvent = utils.mkMembership({
event: true,
mship: "join",
user: userA,
room: roomId,
name: "Alíce\u200b", // note diacritic and zero width char
});
const roomState = {
getStateEvents: function(type) {
if (type !== "m.room.member") {
return [];
}
return [
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userC, name: "Alice",
}),
joinEvent,
];
},
getUserIdsWithDisplayName: function(displayName) {
return [userA, userC];
},
};
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).toNotEqual("Alíce"); // it should disambig.
// user_id should be there somewhere
expect(member.name.indexOf(userA)).toNotEqual(-1);
});
});
});
+223 -17
View File
@@ -11,6 +11,9 @@ describe("RoomState", function() {
const roomId = "!foo:bar";
const userA = "@alice:bar";
const userB = "@bob:bar";
const userC = "@cleo:bar";
const userLazy = "@lazy:bar";
let state;
beforeEach(function() {
@@ -78,8 +81,8 @@ describe("RoomState", function() {
});
describe("getSentinelMember", function() {
it("should return null if there is no member", function() {
expect(state.getSentinelMember("@no-one:here")).toEqual(null);
it("should return a member with the user id as name", function() {
expect(state.getSentinelMember("@no-one:here").name).toEqual("@no-one:here");
});
it("should return a member which doesn't change when the state is updated",
@@ -162,6 +165,7 @@ describe("RoomState", function() {
];
let emitCount = 0;
state.on("RoomState.newMember", function(ev, st, mem) {
expect(state.getMember(mem.userId)).toEqual(mem);
expect(mem.userId).toEqual(memberEvents[emitCount].getSender());
expect(mem.membership).toBeFalsy(); // not defined yet
emitCount += 1;
@@ -222,7 +226,6 @@ describe("RoomState", function() {
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,
});
@@ -262,6 +265,114 @@ describe("RoomState", function() {
});
});
describe("setOutOfBandMembers", function() {
it("should add a new member", function() {
const oobMemberEvent = utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
});
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
const member = state.getMember(userLazy);
expect(member.userId).toEqual(userLazy);
expect(member.isOutOfBand()).toEqual(true);
});
it("should have no effect when not in correct status", function() {
state.setOutOfBandMembers([utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
})]);
expect(state.getMember(userLazy)).toBeFalsy();
});
it("should emit newMember when adding a member", function() {
const userLazy = "@oob:hs";
const oobMemberEvent = utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
});
let eventReceived = false;
state.once('RoomState.newMember', (_, __, member) => {
expect(member.userId).toEqual(userLazy);
eventReceived = true;
});
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
expect(eventReceived).toEqual(true);
});
it("should never overwrite existing members", function() {
const oobMemberEvent = utils.mkMembership({
user: userA, mship: "join", room: roomId, event: true,
});
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
const memberA = state.getMember(userA);
expect(memberA.events.member.getId()).toNotEqual(oobMemberEvent.getId());
expect(memberA.isOutOfBand()).toEqual(false);
});
it("should emit members when updating a member", function() {
const doesntExistYetUserId = "@doesntexistyet:hs";
const oobMemberEvent = utils.mkMembership({
user: doesntExistYetUserId, mship: "join", room: roomId, event: true,
});
let eventReceived = false;
state.once('RoomState.members', (_, __, member) => {
expect(member.userId).toEqual(doesntExistYetUserId);
eventReceived = true;
});
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
expect(eventReceived).toEqual(true);
});
});
describe("clone", function() {
it("should contain same information as original", function() {
// include OOB members in copy
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
})]);
const copy = state.clone();
// check individual members
[userA, userB, userLazy].forEach((userId) => {
const member = state.getMember(userId);
const memberCopy = copy.getMember(userId);
expect(member.name).toEqual(memberCopy.name);
expect(member.isOutOfBand()).toEqual(memberCopy.isOutOfBand());
});
// check member keys
expect(Object.keys(state.members)).toEqual(Object.keys(copy.members));
// check join count
expect(state.getJoinedMemberCount()).toEqual(copy.getJoinedMemberCount());
});
it("should mark old copy as not waiting for out of band anymore", function() {
state.markOutOfBandMembersStarted();
const copy = state.clone();
copy.setOutOfBandMembers([utils.mkMembership({
user: userA, mship: "join", room: roomId, event: true,
})]);
// should have no effect as it should be marked in status finished just like copy
state.setOutOfBandMembers([utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
})]);
expect(state.getMember(userLazy)).toBeFalsy();
});
it("should return copy independent of original", function() {
const copy = state.clone();
copy.setStateEvents([utils.mkMembership({
user: userLazy, mship: "join", room: roomId, event: true,
})]);
expect(state.getMember(userLazy)).toBeFalsy();
expect(state.getJoinedMemberCount()).toEqual(2);
expect(copy.getJoinedMemberCount()).toEqual(3);
});
});
describe("setTypingEvent", function() {
it("should call setTypingEvent on each RoomMember", function() {
const typingEvent = utils.mkEvent({
@@ -284,13 +395,6 @@ describe("RoomState", function() {
});
describe("maySendStateEvent", function() {
it("should say non-joined members may not send state",
function() {
expect(state.maySendStateEvent(
'm.room.name', "@nobody:nowhere",
)).toEqual(false);
});
it("should say any member may send state with no power level event",
function() {
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
@@ -366,15 +470,117 @@ describe("RoomState", function() {
});
});
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);
describe("getJoinedMemberCount", function() {
beforeEach(() => {
state = new RoomState(roomId);
});
it("should update after adding joined member", function() {
state.setStateEvents([
utils.mkMembership({event: true, mship: "join",
user: userA, room: roomId}),
]);
expect(state.getJoinedMemberCount()).toEqual(1);
state.setStateEvents([
utils.mkMembership({event: true, mship: "join",
user: userC, room: roomId}),
]);
expect(state.getJoinedMemberCount()).toEqual(2);
});
});
describe("getInvitedMemberCount", function() {
beforeEach(() => {
state = new RoomState(roomId);
});
it("should update after adding invited member", function() {
state.setStateEvents([
utils.mkMembership({event: true, mship: "invite",
user: userA, room: roomId}),
]);
expect(state.getInvitedMemberCount()).toEqual(1);
state.setStateEvents([
utils.mkMembership({event: true, mship: "invite",
user: userC, room: roomId}),
]);
expect(state.getInvitedMemberCount()).toEqual(2);
});
});
describe("setJoinedMemberCount", function() {
beforeEach(() => {
state = new RoomState(roomId);
});
it("should, once used, override counting members from state", function() {
state.setStateEvents([
utils.mkMembership({event: true, mship: "join",
user: userA, room: roomId}),
]);
expect(state.getJoinedMemberCount()).toEqual(1);
state.setJoinedMemberCount(100);
expect(state.getJoinedMemberCount()).toEqual(100);
state.setStateEvents([
utils.mkMembership({event: true, mship: "join",
user: userC, room: roomId}),
]);
expect(state.getJoinedMemberCount()).toEqual(100);
});
it("should, once used, override counting members from state, " +
"also after clone", function() {
state.setStateEvents([
utils.mkMembership({event: true, mship: "join",
user: userA, room: roomId}),
]);
state.setJoinedMemberCount(100);
const copy = state.clone();
copy.setStateEvents([
utils.mkMembership({event: true, mship: "join",
user: userC, room: roomId}),
]);
expect(state.getJoinedMemberCount()).toEqual(100);
});
});
describe("setInvitedMemberCount", function() {
beforeEach(() => {
state = new RoomState(roomId);
});
it("should, once used, override counting members from state", function() {
state.setStateEvents([
utils.mkMembership({event: true, mship: "invite",
user: userB, room: roomId}),
]);
expect(state.getInvitedMemberCount()).toEqual(1);
state.setInvitedMemberCount(100);
expect(state.getInvitedMemberCount()).toEqual(100);
state.setStateEvents([
utils.mkMembership({event: true, mship: "invite",
user: userC, room: roomId}),
]);
expect(state.getInvitedMemberCount()).toEqual(100);
});
it("should, once used, override counting members from state, " +
"also after clone", function() {
state.setStateEvents([
utils.mkMembership({event: true, mship: "invite",
user: userB, room: roomId}),
]);
state.setInvitedMemberCount(100);
const copy = state.clone();
copy.setStateEvents([
utils.mkMembership({event: true, mship: "invite",
user: userC, room: roomId}),
]);
expect(state.getInvitedMemberCount()).toEqual(100);
});
});
describe("maySendEvent", function() {
it("should say any member may send events with no power level event",
function() {
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
+310 -123
View File
@@ -67,13 +67,14 @@ describe("Room", function() {
describe("getMember", function() {
beforeEach(function() {
// clobber members property with test data
room.currentState.members = {
"@alice:bar": {
userId: userA,
roomId: roomId,
},
};
room.currentState.getMember.andCall(function(userId) {
return {
"@alice:bar": {
userId: userA,
roomId: roomId,
},
}[userId];
});
});
it("should return null if the member isn't in current state", function() {
@@ -386,7 +387,7 @@ describe("Room", function() {
let events = null;
beforeEach(function() {
room = new Room(roomId, {timelineSupport: timelineSupport});
room = new Room(roomId, null, null, {timelineSupport: timelineSupport});
// set events each time to avoid resusing Event objects (which
// doesn't work because they get frozen)
events = [
@@ -468,7 +469,7 @@ describe("Room", function() {
describe("compareEventOrdering", function() {
beforeEach(function() {
room = new Room(roomId, {timelineSupport: true});
room = new Room(roomId, null, null, {timelineSupport: true});
});
const events = [
@@ -570,72 +571,75 @@ describe("Room", function() {
describe("hasMembershipState", function() {
it("should return true for a matching userId and membership",
function() {
room.currentState.members = {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
"@bob:bar": { userId: "@bob:bar", membership: "invite" },
};
room.currentState.getMember.andCall(function(userId) {
return {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
"@bob:bar": { userId: "@bob:bar", membership: "invite" },
}[userId];
});
expect(room.hasMembershipState("@bob:bar", "invite")).toBe(true);
});
it("should return false if match membership but no match userId",
function() {
room.currentState.members = {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
};
room.currentState.getMember.andCall(function(userId) {
return {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
}[userId];
});
expect(room.hasMembershipState("@bob:bar", "join")).toBe(false);
});
it("should return false if match userId but no match membership",
function() {
room.currentState.members = {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
};
room.currentState.getMember.andCall(function(userId) {
return {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
}[userId];
});
expect(room.hasMembershipState("@alice:bar", "ban")).toBe(false);
});
it("should return false if no match membership or userId",
function() {
room.currentState.members = {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
};
room.currentState.getMember.andCall(function(userId) {
return {
"@alice:bar": { userId: "@alice:bar", membership: "join" },
}[userId];
});
expect(room.hasMembershipState("@bob:bar", "invite")).toBe(false);
});
it("should return false if no members exist",
function() {
room.currentState.members = {};
expect(room.hasMembershipState("@foo:bar", "join")).toBe(false);
});
});
describe("recalculate", function() {
let stateLookup = {
// event.type + "$" event.state_key : MatrixEvent
};
const setJoinRule = function(rule) {
stateLookup["m.room.join_rules$"] = utils.mkEvent({
room.addLiveEvents([utils.mkEvent({
type: "m.room.join_rules", room: roomId, user: userA, content: {
join_rule: rule,
}, event: true,
});
})]);
};
const setAliases = function(aliases, stateKey) {
if (!stateKey) {
stateKey = "flibble";
}
stateLookup["m.room.aliases$" + stateKey] = utils.mkEvent({
room.addLiveEvents([utils.mkEvent({
type: "m.room.aliases", room: roomId, skey: stateKey, content: {
aliases: aliases,
}, event: true,
});
})]);
};
const setRoomName = function(name) {
stateLookup["m.room.name$"] = utils.mkEvent({
room.addLiveEvents([utils.mkEvent({
type: "m.room.name", room: roomId, user: userA, content: {
name: name,
}, event: true,
});
})]);
};
const addMember = function(userId, state, opts) {
if (!state) {
@@ -647,56 +651,14 @@ describe("Room", function() {
opts.user = opts.user || userId;
opts.skey = userId;
opts.event = true;
stateLookup["m.room.member$" + userId] = utils.mkMembership(opts);
const event = utils.mkMembership(opts);
room.addLiveEvents([event]);
return event;
};
beforeEach(function() {
stateLookup = {};
room.currentState.getStateEvents.andCall(function(type, key) {
if (key === undefined) {
const prefix = type + "$";
const list = [];
for (const stateBlob in stateLookup) {
if (!stateLookup.hasOwnProperty(stateBlob)) {
continue;
}
if (stateBlob.indexOf(prefix) === 0) {
list.push(stateLookup[stateBlob]);
}
}
return list;
} else {
return stateLookup[type + "$" + key];
}
});
room.currentState.getMembers.andCall(function() {
const memberEvents = room.currentState.getStateEvents("m.room.member");
const members = [];
for (let i = 0; i < memberEvents.length; i++) {
members.push({
name: memberEvents[i].event.content &&
memberEvents[i].event.content.displayname ?
memberEvents[i].event.content.displayname :
memberEvents[i].getStateKey(),
userId: memberEvents[i].getStateKey(),
events: { member: memberEvents[i] },
});
}
return members;
});
room.currentState.getMember.andCall(function(userId) {
const memberEvent = room.currentState.getStateEvents(
"m.room.member", userId,
);
return {
name: memberEvent.event.content &&
memberEvent.event.content.displayname ?
memberEvent.event.content.displayname :
memberEvent.getStateKey(),
userId: memberEvent.getStateKey(),
events: { member: memberEvent },
};
});
// no mocking
room = new Room(roomId, null, userA);
});
describe("Room.recalculate => Stripped State Events", function() {
@@ -704,8 +666,8 @@ describe("Room", function() {
"room is an invite room", function() {
const roomName = "flibble";
addMember(userA, "invite");
stateLookup["m.room.member$" + userA].event.invite_room_state = [
const event = addMember(userA, "invite");
event.event.invite_room_state = [
{
type: "m.room.name",
state_key: "",
@@ -715,30 +677,108 @@ describe("Room", function() {
},
];
room.recalculate(userA);
expect(room.currentState.setStateEvents).toHaveBeenCalled();
// first call, first arg (which is an array), first element in array
const fakeEvent = room.currentState.setStateEvents.calls[0].
arguments[0][0];
expect(fakeEvent.getContent()).toEqual({
name: roomName,
});
room.recalculate();
expect(room.name).toEqual(roomName);
});
it("should not clobber state events if it isn't an invite room", function() {
addMember(userA, "join");
stateLookup["m.room.member$" + userA].event.invite_room_state = [
const event = addMember(userA, "join");
const roomName = "flibble";
setRoomName(roomName);
const roomNameToIgnore = "ignoreme";
event.event.invite_room_state = [
{
type: "m.room.name",
state_key: "",
content: {
name: "flibble",
name: roomNameToIgnore,
},
},
];
room.recalculate(userA);
expect(room.currentState.setStateEvents).toNotHaveBeenCalled();
room.recalculate();
expect(room.name).toEqual(roomName);
});
});
describe("Room.recalculate => Room Name using room summary", function() {
it("should use room heroes if available", function() {
addMember(userA, "invite");
addMember(userB);
addMember(userC);
addMember(userD);
room.setSummary({
"m.heroes": [userB, userC, userD],
});
room.recalculate();
expect(room.name).toEqual(`${userB} and 2 others`);
});
it("missing hero member state reverts to mxid", function() {
room.setSummary({
"m.heroes": [userB],
"m.joined_member_count": 2,
});
room.recalculate();
expect(room.name).toEqual(userB);
});
it("uses hero name from state", function() {
const name = "Mr B";
addMember(userA, "invite");
addMember(userB, "join", {name});
room.setSummary({
"m.heroes": [userB],
});
room.recalculate();
expect(room.name).toEqual(name);
});
it("uses counts from summary", function() {
const name = "Mr B";
addMember(userB, "join", {name});
room.setSummary({
"m.heroes": [userB],
"m.joined_member_count": 50,
"m.invited_member_count": 50,
});
room.recalculate();
expect(room.name).toEqual(`${name} and 98 others`);
});
it("relies on heroes in case of absent counts", function() {
const nameB = "Mr Bean";
const nameC = "Mel C";
addMember(userB, "join", {name: nameB});
addMember(userC, "join", {name: nameC});
room.setSummary({
"m.heroes": [userB, userC],
});
room.recalculate();
expect(room.name).toEqual(`${nameB} and ${nameC}`);
});
it("uses only heroes", function() {
const nameB = "Mr Bean";
addMember(userB, "join", {name: nameB});
addMember(userC, "join");
room.setSummary({
"m.heroes": [userB],
});
room.recalculate();
expect(room.name).toEqual(nameB);
});
it("reverts to empty room in case of self chat", function() {
room.setSummary({
"m.heroes": [],
"m.invited_member_count": 1,
});
room.recalculate();
expect(room.name).toEqual("Empty room");
});
});
@@ -751,7 +791,7 @@ describe("Room", function() {
addMember(userB);
addMember(userC);
addMember(userD);
room.recalculate(userA);
room.recalculate();
const name = room.name;
// we expect at least 1 member to be mentioned
const others = [userB, userC, userD];
@@ -772,7 +812,7 @@ describe("Room", function() {
addMember(userA);
addMember(userB);
addMember(userC);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name.indexOf(userB)).toNotEqual(-1, name);
expect(name.indexOf(userC)).toNotEqual(-1, name);
@@ -785,7 +825,7 @@ describe("Room", function() {
addMember(userA);
addMember(userB);
addMember(userC);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name.indexOf(userB)).toNotEqual(-1, name);
expect(name.indexOf(userC)).toNotEqual(-1, name);
@@ -797,7 +837,7 @@ describe("Room", function() {
setJoinRule("public");
addMember(userA);
addMember(userB);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name.indexOf(userB)).toNotEqual(-1, name);
});
@@ -808,7 +848,7 @@ describe("Room", function() {
setJoinRule("invite");
addMember(userA);
addMember(userB);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name.indexOf(userB)).toNotEqual(-1, name);
});
@@ -818,7 +858,7 @@ describe("Room", function() {
setJoinRule("invite");
addMember(userA, "invite", {user: userB});
addMember(userB);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name.indexOf(userB)).toNotEqual(-1, name);
});
@@ -828,7 +868,7 @@ describe("Room", function() {
const alias = "#room_alias:here";
setJoinRule("invite");
setAliases([alias, "#another:one"]);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name).toEqual(alias);
});
@@ -838,7 +878,7 @@ describe("Room", function() {
const alias = "#room_alias:here";
setJoinRule("public");
setAliases([alias, "#another:one"]);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name).toEqual(alias);
});
@@ -848,7 +888,7 @@ describe("Room", function() {
const roomName = "A mighty name indeed";
setJoinRule("invite");
setRoomName(roomName);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name).toEqual(roomName);
});
@@ -858,25 +898,23 @@ describe("Room", function() {
const roomName = "A mighty name indeed";
setJoinRule("public");
setRoomName(roomName);
room.recalculate(userA);
const name = room.name;
expect(name).toEqual(roomName);
room.recalculate();
expect(room.name).toEqual(roomName);
});
it("should return 'Empty room' for private (invite join_rules) rooms if" +
" a room name and alias don't exist and it is a self-chat.", function() {
setJoinRule("invite");
addMember(userA);
room.recalculate(userA);
const name = room.name;
expect(name).toEqual("Empty room");
room.recalculate();
expect(room.name).toEqual("Empty room");
});
it("should return 'Empty room' for public (public join_rules) rooms if a" +
" room name and alias don't exist and it is a self-chat.", function() {
setJoinRule("public");
addMember(userA);
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name).toEqual("Empty room");
});
@@ -884,7 +922,7 @@ describe("Room", function() {
it("should return 'Empty room' if there is no name, " +
"alias or members in the room.",
function() {
room.recalculate(userA);
room.recalculate();
const name = room.name;
expect(name).toEqual("Empty room");
});
@@ -893,9 +931,9 @@ describe("Room", function() {
"available",
function() {
setJoinRule("invite");
addMember(userA, 'join', {name: "Alice"});
addMember(userB, "invite", {user: userA});
room.recalculate(userB);
addMember(userB, 'join', {name: "Alice"});
addMember(userA, "invite", {user: userA});
room.recalculate();
const name = room.name;
expect(name).toEqual("Alice");
});
@@ -903,11 +941,11 @@ describe("Room", function() {
it("should return inviter mxid if display name not available",
function() {
setJoinRule("invite");
addMember(userA);
addMember(userB, "invite", {user: userA});
room.recalculate(userB);
addMember(userB);
addMember(userA, "invite", {user: userA});
room.recalculate();
const name = room.name;
expect(name).toEqual(userA);
expect(name).toEqual(userB);
});
});
});
@@ -1154,7 +1192,7 @@ describe("Room", function() {
describe("addPendingEvent", function() {
it("should add pending events to the pendingEventList if " +
"pendingEventOrdering == 'detached'", function() {
const room = new Room(roomId, {
const room = new Room(roomId, null, userA, {
pendingEventOrdering: "detached",
});
const eventA = utils.mkMessage({
@@ -1180,7 +1218,7 @@ describe("Room", function() {
it("should add pending events to the timeline if " +
"pendingEventOrdering == 'chronological'", function() {
room = new Room(roomId, {
room = new Room(roomId, null, userA, {
pendingEventOrdering: "chronological",
});
const eventA = utils.mkMessage({
@@ -1204,7 +1242,7 @@ describe("Room", function() {
describe("updatePendingEvent", function() {
it("should remove cancelled events from the pending list", function() {
const room = new Room(roomId, {
const room = new Room(roomId, null, userA, {
pendingEventOrdering: "detached",
});
const eventA = utils.mkMessage({
@@ -1240,7 +1278,7 @@ describe("Room", function() {
it("should remove cancelled events from the timeline", function() {
const room = new Room(roomId);
const room = new Room(roomId, null, userA);
const eventA = utils.mkMessage({
room: roomId, user: userA, event: true,
});
@@ -1272,4 +1310,153 @@ describe("Room", function() {
expect(callCount).toEqual(1);
});
});
describe("loadMembersIfNeeded", function() {
function createClientMock(serverResponse, storageResponse = null) {
return {
getEventMapper: function() {
// events should already be MatrixEvents
return function(event) {return event;};
},
isRoomEncrypted: function() {
return false;
},
_http: {
serverResponse,
authedRequest: function() {
if (this.serverResponse instanceof Error) {
return Promise.reject(this.serverResponse);
} else {
return Promise.resolve({chunk: this.serverResponse});
}
},
},
store: {
storageResponse,
storedMembers: null,
getOutOfBandMembers: function() {
if (this.storageResponse instanceof Error) {
return Promise.reject(this.storageResponse);
} else {
return Promise.resolve(this.storageResponse);
}
},
setOutOfBandMembers: function(roomId, memberEvents) {
this.storedMembers = memberEvents;
return Promise.resolve();
},
getSyncToken: () => "sync_token",
},
};
}
const memberEvent = utils.mkMembership({
user: "@user_a:bar", mship: "join",
room: roomId, event: true, name: "User A",
});
it("should load members from server on first call", async function() {
const client = createClientMock([memberEvent]);
const room = new Room(roomId, client, null, {lazyLoadMembers: true});
await room.loadMembersIfNeeded();
const memberA = room.getMember("@user_a:bar");
expect(memberA.name).toEqual("User A");
const storedMembers = client.store.storedMembers;
expect(storedMembers.length).toEqual(1);
expect(storedMembers[0].event_id).toEqual(memberEvent.getId());
});
it("should take members from storage if available", async function() {
const memberEvent2 = utils.mkMembership({
user: "@user_a:bar", mship: "join",
room: roomId, event: true, name: "Ms A",
});
const client = createClientMock([memberEvent2], [memberEvent]);
const room = new Room(roomId, client, null, {lazyLoadMembers: true});
await room.loadMembersIfNeeded();
const memberA = room.getMember("@user_a:bar");
expect(memberA.name).toEqual("User A");
});
it("should allow retry on error", async function() {
const client = createClientMock(new Error("server says no"));
const room = new Room(roomId, client, null, {lazyLoadMembers: true});
let hasThrown = false;
try {
await room.loadMembersIfNeeded();
} catch(err) {
hasThrown = true;
}
expect(hasThrown).toEqual(true);
client._http.serverResponse = [memberEvent];
await room.loadMembersIfNeeded();
const memberA = room.getMember("@user_a:bar");
expect(memberA.name).toEqual("User A");
});
});
describe("getMyMembership", function() {
it("should return synced membership if membership isn't available yet",
function() {
const room = new Room(roomId, null, userA);
room.updateMyMembership("invite");
expect(room.getMyMembership()).toEqual("invite");
});
it("should emit a Room.myMembership event on a change",
function() {
const room = new Room(roomId, null, userA);
const events = [];
room.on("Room.myMembership", (_room, membership, oldMembership) => {
events.push({membership, oldMembership});
});
room.updateMyMembership("invite");
expect(room.getMyMembership()).toEqual("invite");
expect(events[0]).toEqual({membership: "invite", oldMembership: null});
events.splice(0); //clear
room.updateMyMembership("invite");
expect(events.length).toEqual(0);
room.updateMyMembership("join");
expect(room.getMyMembership()).toEqual("join");
expect(events[0]).toEqual({membership: "join", oldMembership: "invite"});
});
});
describe("guessDMUserId", function() {
it("should return first hero id",
function() {
const room = new Room(roomId, null, userA);
room.setSummary({'m.heroes': [userB]});
expect(room.guessDMUserId()).toEqual(userB);
});
it("should return first member that isn't self",
function() {
const room = new Room(roomId, null, userA);
room.addLiveEvents([utils.mkMembership({
user: userB, mship: "join",
room: roomId, event: true,
})]);
expect(room.guessDMUserId()).toEqual(userB);
});
it("should return self if only member present",
function() {
const room = new Room(roomId, null, userA);
expect(room.guessDMUserId()).toEqual(userA);
});
});
describe("maySendMessage", function() {
it("should return false if synced membership not join",
function() {
const room = new Room(roomId, null, userA);
room.updateMyMembership("invite");
expect(room.maySendMessage()).toEqual(false);
room.updateMyMembership("leave");
expect(room.maySendMessage()).toEqual(false);
room.updateMyMembership("join");
expect(room.maySendMessage()).toEqual(true);
});
});
});
+57
View File
@@ -52,6 +52,11 @@ describe("SyncAccumulator", function() {
member("bob", "join"),
],
},
summary: {
"m.heroes": undefined,
"m.joined_member_count": undefined,
"m.invited_member_count": undefined,
},
timeline: {
events: [msg("alice", "hi")],
prev_batch: "something",
@@ -318,6 +323,58 @@ describe("SyncAccumulator", function() {
},
});
});
describe("summary field", function() {
function createSyncResponseWithSummary(summary) {
return {
next_batch: "abc",
rooms: {
invite: {},
leave: {},
join: {
"!foo:bar": {
account_data: { events: [] },
ephemeral: { events: [] },
unread_notifications: {},
state: {
events: [],
},
summary: summary,
timeline: {
events: [],
prev_batch: "something",
},
},
},
},
};
}
it("should copy summary properties", function() {
sa.accumulate(createSyncResponseWithSummary({
"m.heroes": ["@alice:bar"],
"m.invited_member_count": 2,
}));
const summary = sa.getJSON().roomsData.join["!foo:bar"].summary;
expect(summary["m.invited_member_count"]).toEqual(2);
expect(summary["m.heroes"]).toEqual(["@alice:bar"]);
});
it("should accumulate summary properties", function() {
sa.accumulate(createSyncResponseWithSummary({
"m.heroes": ["@alice:bar"],
"m.invited_member_count": 2,
}));
sa.accumulate(createSyncResponseWithSummary({
"m.heroes": ["@bob:bar"],
"m.joined_member_count": 5,
}));
const summary = sa.getJSON().roomsData.join["!foo:bar"].summary;
expect(summary["m.invited_member_count"]).toEqual(2);
expect(summary["m.joined_member_count"]).toEqual(5);
expect(summary["m.heroes"]).toEqual(["@bob:bar"]);
});
});
});
function syncSkeleton(joinObj) {
+389
View File
@@ -0,0 +1,389 @@
/*
Copyright 2018 New Vector 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 auto-discovery */
import Promise from 'bluebird';
const logger = require("./logger");
import { URL as NodeURL } from "url";
// Dev note: Auto discovery is part of the spec.
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
/**
* Description for what an automatically discovered client configuration
* would look like. Although this is a class, it is recommended that it
* be treated as an interface definition rather than as a class.
*
* Additional properties than those defined here may be present, and
* should follow the Java package naming convention.
*/
class DiscoveredClientConfig { // eslint-disable-line no-unused-vars
// Dev note: this is basically a copy/paste of the .well-known response
// object as defined in the spec. It does have additional information,
// however. Overall, this exists to serve as a place for documentation
// and not functionality.
// See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-well-known-matrix-client
constructor() {
/**
* The homeserver configuration the client should use. This will
* always be present on the object.
* @type {{state: string, base_url: string}} The configuration.
*/
this["m.homeserver"] = {
/**
* The lookup result state. If this is anything other than
* AutoDiscovery.SUCCESS then base_url may be falsey. Additionally,
* if this is not AutoDiscovery.SUCCESS then the client should
* assume the other properties in the client config (such as
* the identity server configuration) are not valid.
*/
state: AutoDiscovery.PROMPT,
/**
* If the state is AutoDiscovery.FAIL_ERROR or .FAIL_PROMPT
* then this will contain a human-readable (English) message
* for what went wrong. If the state is none of those previously
* mentioned, this will be falsey.
*/
error: "Something went wrong",
/**
* The base URL clients should use to talk to the homeserver,
* particularly for the login process. May be falsey if the
* state is not AutoDiscovery.SUCCESS.
*/
base_url: "https://matrix.org",
};
/**
* The identity server configuration the client should use. This
* will always be present on teh object.
* @type {{state: string, base_url: string}} The configuration.
*/
this["m.identity_server"] = {
/**
* The lookup result state. If this is anything other than
* AutoDiscovery.SUCCESS then base_url may be falsey.
*/
state: AutoDiscovery.PROMPT,
/**
* The base URL clients should use for interacting with the
* identity server. May be falsey if the state is not
* AutoDiscovery.SUCCESS.
*/
base_url: "https://vector.im",
};
}
}
/**
* Utilities for automatically discovery resources, such as homeservers
* for users to log in to.
*/
export class AutoDiscovery {
// Dev note: the constants defined here are related to but not
// exactly the same as those in the spec. This is to hopefully
// translate the meaning of the states in the spec, but also
// support our own if needed.
/**
* The auto discovery failed. The client is expected to communicate
* the error to the user and refuse logging in.
* @return {string}
* @constructor
*/
static get FAIL_ERROR() { return "FAIL_ERROR"; }
/**
* The auto discovery failed, however the client may still recover
* from the problem. The client is recommended to that the same
* action it would for PROMPT while also warning the user about
* what went wrong. The client may also treat this the same as
* a FAIL_ERROR state.
* @return {string}
* @constructor
*/
static get FAIL_PROMPT() { return "FAIL_PROMPT"; }
/**
* The auto discovery didn't fail but did not find anything of
* interest. The client is expected to prompt the user for more
* information, or fail if it prefers.
* @return {string}
* @constructor
*/
static get PROMPT() { return "PROMPT"; }
/**
* The auto discovery was successful.
* @return {string}
* @constructor
*/
static get SUCCESS() { return "SUCCESS"; }
/**
* Attempts to automatically discover client configuration information
* prior to logging in. Such information includes the homeserver URL
* and identity server URL the client would want. Additional details
* may also be discovered, and will be transparently included in the
* response object unaltered.
* @param {string} domain The homeserver domain to perform discovery
* on. For example, "matrix.org".
* @return {Promise<DiscoveredClientConfig>} Resolves to the discovered
* configuration, which may include error states. Rejects on unexpected
* failure, not when discovery fails.
*/
static async findClientConfig(domain) {
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
}
// We use a .well-known lookup for all cases. According to the spec, we
// can do other discovery mechanisms if we want such as custom lookups
// however we won't bother with that here (mostly because the spec only
// supports .well-known right now).
//
// By using .well-known, we need to ensure we at least pull out a URL
// for the homeserver. We don't really need an identity server configuration
// but will return one anyways (with state PROMPT) to make development
// easier for clients. If we can't get a homeserver URL, all bets are
// off on the rest of the config and we'll assume it is invalid too.
// We default to an error state to make the first few checks easier to
// write. We'll update the properties of this object over the duration
// of this function.
const clientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: "Invalid homeserver discovery response",
base_url: null,
},
"m.identity_server": {
// Technically, we don't have a problem with the identity server
// config at this point.
state: AutoDiscovery.PROMPT,
error: null,
base_url: null,
},
};
// Step 1: Actually request the .well-known JSON file and make sure it
// at least has a homeserver definition.
const wellknown = await this._fetchWellKnownObject(
`https://${domain}/.well-known/matrix/client`,
);
if (!wellknown || wellknown.action !== "SUCCESS"
|| !wellknown.raw["m.homeserver"]
|| !wellknown.raw["m.homeserver"]["base_url"]) {
logger.error("No m.homeserver key in well-known response");
if (wellknown.reason) logger.error(wellknown.reason);
if (wellknown.action === "IGNORE") {
clientConfig["m.homeserver"] = {
state: AutoDiscovery.PROMPT,
error: null,
base_url: null,
};
} else {
// this can only ever be FAIL_PROMPT at this point.
clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
}
return Promise.resolve(clientConfig);
}
// Step 2: Make sure the homeserver URL is valid *looking*. We'll make
// sure it points to a homeserver in Step 3.
const hsUrl = this._sanitizeWellKnownUrl(
wellknown.raw["m.homeserver"]["base_url"],
);
if (!hsUrl) {
logger.error("Invalid base_url for m.homeserver");
return Promise.resolve(clientConfig);
}
// Step 3: Make sure the homeserver URL points to a homeserver.
const hsVersions = await this._fetchWellKnownObject(
`${hsUrl}/_matrix/client/versions`,
);
if (!hsVersions || !hsVersions.raw["versions"]) {
logger.error("Invalid /versions response");
return Promise.resolve(clientConfig);
}
// Step 4: Now that the homeserver looks valid, update our client config.
clientConfig["m.homeserver"] = {
state: AutoDiscovery.SUCCESS,
error: null,
base_url: hsUrl,
};
// Step 5: Try to pull out the identity server configuration
let isUrl = "";
if (wellknown.raw["m.identity_server"]) {
// We prepare a failing identity server response to save lines later
// in this branch. Note that we also fail the homeserver check in the
// object because according to the spec we're supposed to FAIL_ERROR
// if *anything* goes wrong with the IS validation, including invalid
// format. This means we're supposed to stop discovery completely.
const failingClientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: "Invalid identity server discovery response",
// We'll provide the base_url that was previously valid for
// debugging purposes.
base_url: clientConfig["m.homeserver"].base_url,
},
"m.identity_server": {
state: AutoDiscovery.FAIL_ERROR,
error: "Invalid identity server discovery response",
base_url: null,
},
};
// Step 5a: Make sure the URL is valid *looking*. We'll make sure it
// points to an identity server in Step 5b.
isUrl = this._sanitizeWellKnownUrl(
wellknown.raw["m.identity_server"]["base_url"],
);
if (!isUrl) {
logger.error("Invalid base_url for m.identity_server");
return Promise.resolve(failingClientConfig);
}
// Step 5b: Verify there is an identity server listening on the provided
// URL.
const isResponse = await this._fetchWellKnownObject(
`${isUrl}/_matrix/identity/api/v1`,
);
if (!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS") {
logger.error("Invalid /api/v1 response");
return Promise.resolve(failingClientConfig);
}
}
// Step 6: Now that the identity server is valid, or never existed,
// populate the IS section.
if (isUrl && isUrl.length > 0) {
clientConfig["m.identity_server"] = {
state: AutoDiscovery.SUCCESS,
error: null,
base_url: isUrl,
};
}
// Step 7: Copy any other keys directly into the clientConfig. This is for
// things like custom configuration of services.
Object.keys(wellknown.raw)
.filter((k) => k !== "m.homeserver" && k !== "m.identity_server")
.map((k) => clientConfig[k] = wellknown.raw[k]);
// Step 8: Give the config to the caller (finally)
return Promise.resolve(clientConfig);
}
/**
* Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and
* is suitable for the requirements laid out by .well-known auto discovery.
* If valid, the URL will also be stripped of any trailing slashes.
* @param {string} url The potentially invalid URL to sanitize.
* @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
* @private
*/
static _sanitizeWellKnownUrl(url) {
if (!url) return false;
try {
// We have to try and parse the URL using the NodeJS URL
// library if we're on NodeJS and use the browser's URL
// library when we're in a browser. To accomplish this, we
// try the NodeJS version first and fall back to the browser.
let parsed = null;
try {
if (NodeURL) parsed = new NodeURL(url);
else parsed = new URL(url);
} catch (e) {
parsed = new URL(url);
}
if (!parsed || !parsed.hostname) return false;
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
const port = parsed.port ? `:${parsed.port}` : "";
const path = parsed.pathname ? parsed.pathname : "";
let saferUrl = `${parsed.protocol}//${parsed.hostname}${port}${path}`;
if (saferUrl.endsWith("/")) {
saferUrl = saferUrl.substring(0, saferUrl.length - 1);
}
return saferUrl;
} catch (e) {
logger.error(e);
return false;
}
}
/**
* Fetches a JSON object from a given URL, as expected by all .well-known
* related lookups. If the server gives a 404 then the `action` will be
* IGNORE. If the server returns something that isn't JSON, the `action`
* will be FAIL_PROMPT. For any other failure the `action` will be FAIL_PROMPT.
*
* The returned object will be a result of the call in object form with
* the following properties:
* raw: The JSON object returned by the server.
* action: One of SUCCESS, IGNORE, or FAIL_PROMPT.
* reason: Relatively human readable description of what went wrong.
* error: The actual Error, if one exists.
* @param {string} url The URL to fetch a JSON object from.
* @return {Promise<object>} Resolves to the returned state.
* @private
*/
static async _fetchWellKnownObject(url) {
return new Promise(function(resolve, reject) {
const request = require("./matrix").getRequest();
if (!request) throw new Error("No request library available");
request(
{ method: "GET", uri: url },
(err, response, body) => {
if (err || response.statusCode < 200 || response.statusCode >= 300) {
let action = "FAIL_PROMPT";
let reason = (err ? err.message : null) || "General failure";
if (response.statusCode === 404) {
action = "IGNORE";
reason = "No .well-known JSON file found";
}
resolve({raw: {}, action: action, reason: reason, error: err});
return;
}
try {
resolve({raw: JSON.parse(body), action: "SUCCESS"});
} catch (e) {
let reason = "General failure";
if (e.name === "SyntaxError") reason = "Invalid JSON";
resolve({
raw: {},
action: "FAIL_PROMPT",
reason: reason, error: e,
});
}
},
);
});
}
}
+157 -12
View File
@@ -262,7 +262,19 @@ MatrixBaseApis.prototype.login = function(loginType, data, callback) {
utils.extend(login_data, data);
return this._http.authedRequest(
callback, "POST", "/login", undefined, login_data,
(error, response) => {
if (loginType === "m.login.password" && response &&
response.access_token && response.user_id) {
this._http.opts.accessToken = response.access_token;
this.credentials = {
userId: response.user_id,
};
}
if (callback) {
callback(error, response);
}
}, "POST", "/login", undefined, login_data,
);
};
@@ -298,9 +310,23 @@ MatrixBaseApis.prototype.loginWithSAML2 = function(relayState, callback) {
* @return {string} The HS URL to hit to begin the CAS login process.
*/
MatrixBaseApis.prototype.getCasLoginUrl = function(redirectUrl) {
return this._http.getUrl("/login/cas/redirect", {
return this.getSsoLoginUrl(redirectUrl, "cas");
};
/**
* @param {string} redirectUrl The URL to redirect to after the HS
* authenticates with the SSO.
* @param {string} loginType The type of SSO login we are doing (sso or cas).
* Defaults to 'sso'.
* @return {string} The HS URL to hit to begin the SSO login process.
*/
MatrixBaseApis.prototype.getSsoLoginUrl = function(redirectUrl, loginType) {
if (loginType === undefined) {
loginType = "sso";
}
return this._http.getUrl("/login/"+loginType+"/redirect", {
"redirectUrl": redirectUrl,
}, httpApi.PREFIX_UNSTABLE);
}, httpApi.PREFIX_R0);
};
/**
@@ -338,18 +364,29 @@ MatrixBaseApis.prototype.logout = function(callback) {
* it is up to the caller to either reset or destroy the MatrixClient after
* this method succeeds.
* @param {object} auth Optional. Auth data to supply for User-Interactive auth.
* @param {module:client.callback} callback Optional.
* @param {boolean} erase Optional. If set, send as `erase` attribute in the
* JSON request body, indicating whether the account should be erased. Defaults
* to false.
* @return {module:client.Promise} Resolves: On success, the empty object
*/
MatrixBaseApis.prototype.deactivateAccount = function(auth, callback) {
let body = {};
if (auth) {
body = {
auth: auth,
};
MatrixBaseApis.prototype.deactivateAccount = function(auth, erase) {
if (typeof(erase) === 'function') {
throw new Error(
'deactivateAccount no longer accepts a callback parameter',
);
}
const body = {};
if (auth) {
body.auth = auth;
}
if (erase !== undefined) {
body.erase = erase;
}
return this._http.authedRequestWithPrefix(
callback, "POST", '/account/deactivate', undefined, body, httpApi.PREFIX_UNSTABLE,
undefined, "POST", '/account/deactivate', undefined, body,
httpApi.PREFIX_R0,
);
};
@@ -406,6 +443,69 @@ MatrixBaseApis.prototype.roomState = function(roomId, callback) {
return this._http.authedRequest(callback, "GET", path);
};
/**
* Get an event in a room by its event id.
* @param {string} roomId
* @param {string} eventId
* @param {module:client.callback} callback Optional.
*
* @return {Promise} Resolves to an object containing the event.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.fetchRoomEvent = function(roomId, eventId, callback) {
const path = utils.encodeUri(
"/rooms/$roomId/event/$eventId", {
$roomId: roomId,
$eventId: eventId,
},
);
return this._http.authedRequest(callback, "GET", path);
};
/**
* @param {string} roomId
* @param {string} includeMembership the membership type to include in the response
* @param {string} excludeMembership the membership type to exclude from the response
* @param {string} atEventId the id of the event for which moment in the timeline the members should be returned for
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: dictionary of userid to profile information
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.members =
function(roomId, includeMembership, excludeMembership, atEventId, callback) {
const queryParams = {};
if (includeMembership) {
queryParams.membership = includeMembership;
}
if (excludeMembership) {
queryParams.not_membership = excludeMembership;
}
if (atEventId) {
queryParams.at = atEventId;
}
const queryString = utils.encodeParams(queryParams);
const path = utils.encodeUri("/rooms/$roomId/members?" + queryString,
{$roomId: roomId});
return this._http.authedRequest(callback, "GET", path);
};
/**
* Upgrades a room to a new protocol version
* @param {string} roomId
* @param {string} newVersion The target version to upgrade to
* @return {module:client.Promise} Resolves: Object with key 'replacement_room'
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.upgradeRoom = function(roomId, newVersion) {
const path = utils.encodeUri("/rooms/$roomId/upgrade", {$roomId: roomId});
return this._http.authedRequest(
undefined, "POST", path, undefined, {new_version: newVersion},
);
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group summary object
@@ -853,6 +953,28 @@ MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
);
};
/**
* @return {module:client.Promise} Resolves: A list of the user's current rooms
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getJoinedRooms = function() {
const path = utils.encodeUri("/joined_rooms");
return this._http.authedRequest(undefined, "GET", path);
};
/**
* Retrieve membership info. for a room.
* @param {string} roomId ID of the room to get membership for
* @return {module:client.Promise} Resolves: A list of currently joined users
* and their profile data.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getJoinedRoomMembers = function(roomId) {
const path = utils.encodeUri("/rooms/$roomId/joined_members", {
$roomId: roomId,
});
return this._http.authedRequest(undefined, "GET", path);
};
// Room Directory operations
// =========================
@@ -1060,6 +1182,10 @@ MatrixBaseApis.prototype.searchUserDirectory = function(opts) {
* @param {string=} opts.name Name to give the file on the server. Defaults
* to <tt>file.name</tt>.
*
* @param {boolean=} opts.includeFilename if false will not send the filename,
* e.g for encrypted file uploads where filename leaks are undesirable.
* Defaults to true.
*
* @param {string=} opts.type Content-type for the upload. Defaults to
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
*
@@ -1707,7 +1833,7 @@ MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
* Get information on how a specific place on a third party protocol
* may be reached.
* @param {string} protocol The protocol given in getThirdpartyProtocols()
* @param {object} params Protocol-specific parameters, as given in th
* @param {object} params Protocol-specific parameters, as given in the
* response to getThirdpartyProtocols()
* @return {module:client.Promise} Resolves to the result object
*/
@@ -1722,6 +1848,25 @@ MatrixBaseApis.prototype.getThirdpartyLocation = function(protocol, params) {
);
};
/**
* Get information on how a specific user on a third party protocol
* may be reached.
* @param {string} protocol The protocol given in getThirdpartyProtocols()
* @param {object} params Protocol-specific parameters, as given in the
* response to getThirdpartyProtocols()
* @return {module:client.Promise} Resolves to the result object
*/
MatrixBaseApis.prototype.getThirdpartyUser = function(protocol, params) {
const path = utils.encodeUri("/thirdparty/user/$protocol", {
$protocol: protocol,
});
return this._http.authedRequestWithPrefix(
undefined, "GET", path, params, undefined,
httpApi.PREFIX_UNSTABLE,
);
};
/**
* MatrixBaseApis object
*/
+837 -136
View File
File diff suppressed because it is too large Load Diff
+83 -27
View File
@@ -24,6 +24,7 @@ limitations under the License.
import Promise from 'bluebird';
import logger from '../logger';
import DeviceInfo from './deviceinfo';
import olmlib from './olmlib';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
@@ -71,6 +72,9 @@ export default class DeviceList {
// }
this._devices = {};
// map of identity keys to the user who owns it
this._userByIdentityKey = {};
// which users we are tracking device status for.
// userId -> TRACKING_STATUS_*
this._deviceTrackingStatus = {}; // loaded from storage in load()
@@ -110,7 +114,7 @@ export default class DeviceList {
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
if (deviceData === null) {
console.log("Migrating e2e device data...");
logger.log("Migrating e2e device data...");
this._devices = this._sessionStore.getAllEndToEndDevices() || {};
this._deviceTrackingStatus = (
this._sessionStore.getEndToEndDeviceTrackingStatus() || {}
@@ -128,6 +132,16 @@ export default class DeviceList {
deviceData.trackingStatus : {};
this._syncToken = deviceData ? deviceData.syncToken : null;
}
this._userByIdentityKey = {};
for (const user of Object.keys(this._devices)) {
const userDevices = this._devices[user];
for (const device of Object.keys(userDevices)) {
const idKey = userDevices[device].keys['curve25519:'+device];
if (idKey !== undefined) {
this._userByIdentityKey[idKey] = user;
}
}
}
});
},
);
@@ -145,6 +159,12 @@ export default class DeviceList {
}
}
stop() {
if (this._saveTimer !== null) {
clearTimeout(this._saveTimer);
}
}
/**
* Save the device tracking state to storage, if any changes are
* pending other than updating the sync token
@@ -190,7 +210,7 @@ export default class DeviceList {
const resolveSavePromise = this._resolveSavePromise;
this._savePromiseTime = targetTime;
this._saveTimer = setTimeout(() => {
console.log('Saving device tracking data at token ' + this._syncToken);
logger.log('Saving device tracking data at token ' + this._syncToken);
// null out savePromise now (after the delay but before the write),
// otherwise we could return the existing promise when the save has
// actually already happened. Likewise for the dirty flag.
@@ -258,7 +278,7 @@ export default class DeviceList {
if (this._keyDownloadsInProgressByUser[u]) {
// already a key download in progress/queued for this user; its results
// will be good enough for us.
console.log(
logger.log(
`downloadKeys: already have a download in progress for ` +
`${u}: awaiting its result`,
);
@@ -269,13 +289,13 @@ export default class DeviceList {
});
if (usersToDownload.length != 0) {
console.log("downloadKeys: downloading for", usersToDownload);
logger.log("downloadKeys: downloading for", usersToDownload);
const downloadPromise = this._doKeyDownload(usersToDownload);
promises.push(downloadPromise);
}
if (promises.length === 0) {
console.log("downloadKeys: already have all necessary keys");
logger.log("downloadKeys: already have all necessary keys");
}
return Promise.all(promises).then(() => {
@@ -357,13 +377,17 @@ export default class DeviceList {
/**
* Find a device by curve25519 identity key
*
* @param {string} userId owner of the device
* @param {string} algorithm encryption algorithm
* @param {string} senderKey curve25519 key to match
*
* @return {module:crypto/deviceinfo?}
*/
getDeviceByIdentityKey(userId, algorithm, senderKey) {
getDeviceByIdentityKey(algorithm, senderKey) {
const userId = this._userByIdentityKey[senderKey];
if (!userId) {
return null;
}
if (
algorithm !== olmlib.OLM_ALGORITHM &&
algorithm !== olmlib.MEGOLM_ALGORITHM
@@ -408,7 +432,23 @@ export default class DeviceList {
* @param {Object} devs New device info for user
*/
storeDevicesForUser(u, devs) {
// remove previous devices from _userByIdentityKey
if (this._devices[u] !== undefined) {
for (const [deviceId, dev] of Object.entries(this._devices[u])) {
const identityKey = dev.keys['curve25519:'+deviceId];
delete this._userByIdentityKey[identityKey];
}
}
this._devices[u] = devs;
// add new ones
for (const [deviceId, dev] of Object.entries(devs)) {
const identityKey = dev.keys['curve25519:'+deviceId];
this._userByIdentityKey[identityKey] = u;
}
this._dirty = true;
}
@@ -433,12 +473,12 @@ export default class DeviceList {
throw new Error('userId must be a string; was '+userId);
}
if (!this._deviceTrackingStatus[userId]) {
console.log('Now tracking device list for ' + userId);
logger.log('Now tracking device list for ' + userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
// we don't yet persist the tracking status, since there may be a lot
// of calls; we save all data together once the sync is done
this._dirty = true;
}
// we don't yet persist the tracking status, since there may be a lot
// of calls; we save all data together once the sync is done
this._dirty = true;
}
/**
@@ -452,7 +492,7 @@ export default class DeviceList {
*/
stopTrackingDeviceList(userId) {
if (this._deviceTrackingStatus[userId]) {
console.log('No longer tracking device list for ' + userId);
logger.log('No longer tracking device list for ' + userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED;
// we don't yet persist the tracking status, since there may be a lot
@@ -487,7 +527,7 @@ export default class DeviceList {
*/
invalidateUserDeviceList(userId) {
if (this._deviceTrackingStatus[userId]) {
console.log("Marking device list outdated for", userId);
logger.log("Marking device list outdated for", userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_PENDING_DOWNLOAD;
// we don't yet persist the tracking status, since there may be a lot
@@ -525,7 +565,23 @@ export default class DeviceList {
* @param {Object} devices deviceId->{object} the new devices
*/
_setRawStoredDevicesForUser(userId, devices) {
// remove old devices from _userByIdentityKey
if (this._devices[userId] !== undefined) {
for (const [deviceId, dev] of Object.entries(this._devices[userId])) {
const identityKey = dev.keys['curve25519:'+deviceId];
delete this._userByIdentityKey[identityKey];
}
}
this._devices[userId] = devices;
// add new devices into _userByIdentityKey
for (const [deviceId, dev] of Object.entries(devices)) {
const identityKey = dev.keys['curve25519:'+deviceId];
this._userByIdentityKey[identityKey] = userId;
}
}
/**
@@ -550,7 +606,7 @@ export default class DeviceList {
).then(() => {
finished(true);
}, (e) => {
console.error(
logger.error(
'Error downloading keys for ' + users + ":", e,
);
finished(false);
@@ -573,7 +629,7 @@ export default class DeviceList {
// since we started this request. If that happens, we should
// ignore the completion of the first one.
if (this._keyDownloadsInProgressByUser[u] !== prom) {
console.log('Another update in the queue for', u,
logger.log('Another update in the queue for', u,
'- not marking up-to-date');
return;
}
@@ -584,7 +640,7 @@ export default class DeviceList {
// we didn't get any new invalidations since this download started:
// this user's device list is now up to date.
this._deviceTrackingStatus[u] = TRACKING_STATUS_UP_TO_DATE;
console.log("Device list for", u, "now up to date");
logger.log("Device list for", u, "now up to date");
} else {
this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
}
@@ -659,7 +715,7 @@ class DeviceListUpdateSerialiser {
if (this._downloadInProgress) {
// just queue up these users
console.log('Queued key download for', users);
logger.log('Queued key download for', users);
return this._queuedQueryDeferred.promise;
}
@@ -679,7 +735,7 @@ class DeviceListUpdateSerialiser {
const deferred = this._queuedQueryDeferred;
this._queuedQueryDeferred = null;
console.log('Starting key download for', downloadUsers);
logger.log('Starting key download for', downloadUsers);
this._downloadInProgress = true;
const opts = {};
@@ -706,7 +762,7 @@ class DeviceListUpdateSerialiser {
return prom;
}).done(() => {
console.log('Completed key download for ' + downloadUsers);
logger.log('Completed key download for ' + downloadUsers);
this._downloadInProgress = false;
deferred.resolve();
@@ -716,7 +772,7 @@ class DeviceListUpdateSerialiser {
this._doQueuedQueries();
}
}, (e) => {
console.warn('Error downloading keys for ' + downloadUsers + ':', e);
logger.warn('Error downloading keys for ' + downloadUsers + ':', e);
this._downloadInProgress = false;
deferred.reject(e);
});
@@ -725,7 +781,7 @@ class DeviceListUpdateSerialiser {
}
async _processQueryResponseForUser(userId, response) {
console.log('got keys for ' + userId + ':', response);
logger.log('got keys for ' + userId + ':', response);
// map from deviceid -> deviceinfo for this user
const userStore = {};
@@ -763,7 +819,7 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
}
if (!(deviceId in userResult)) {
console.log("Device " + userId + ":" + deviceId +
logger.log("Device " + userId + ":" + deviceId +
" has been removed");
delete userStore[deviceId];
updated = true;
@@ -780,12 +836,12 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
// 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 +
logger.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 +
logger.warn("Mismatched device_id " + deviceResult.device_id +
" in keys from " + userId + ":" + deviceId);
continue;
}
@@ -815,7 +871,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
const signKeyId = "ed25519:" + deviceId;
const signKey = deviceResult.keys[signKeyId];
if (!signKey) {
console.warn("Device " + userId + ":" + deviceId +
logger.warn("Device " + userId + ":" + deviceId +
" has no ed25519 key");
return false;
}
@@ -825,7 +881,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
try {
await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
} catch (e) {
console.warn("Unable to verify signature on device " +
logger.warn("Unable to verify signature on device " +
userId + ":" + deviceId + ":" + e);
return false;
}
@@ -842,7 +898,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
// best off sticking with the original keys.
//
// Should we warn the user about it somehow?
console.warn("Ed25519 key for device " + userId + ":" +
logger.warn("Ed25519 key for device " + userId + ":" +
deviceId + " has changed");
return false;
}
+113 -69
View File
@@ -15,19 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import logger from '../logger';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
/**
* olm.js wrapper
*
* @module crypto/OlmDevice
*/
const Olm = global.Olm;
if (!Olm) {
throw new Error("global.Olm is not defined");
}
// 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;
@@ -127,7 +117,7 @@ OlmDevice.prototype.init = async function() {
await this._migrateFromSessionStore();
let e2eKeys;
const account = new Olm.Account();
const account = new global.Olm.Account();
try {
await _initialiseAccount(
this._sessionStore, this._cryptoStore, this._pickleKey, account,
@@ -161,7 +151,7 @@ async function _initialiseAccount(sessionStore, cryptoStore, pickleKey, account)
* @return {array} The version of Olm.
*/
OlmDevice.getOlmVersion = function() {
return Olm.get_library_version();
return global.Olm.get_library_version();
};
OlmDevice.prototype._migrateFromSessionStore = async function() {
@@ -173,7 +163,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
// Migrate from sessionStore
pickledAccount = this._sessionStore.getEndToEndAccount();
if (pickledAccount !== null) {
console.log("Migrating account from session store");
logger.log("Migrating account from session store");
this._cryptoStore.storeAccount(txn, pickledAccount);
}
}
@@ -195,7 +185,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
// has run against the same localstorage and created some spurious sessions.
this._cryptoStore.countEndToEndSessions(txn, (count) => {
if (count) {
console.log("Crypto store already has sessions: not migrating");
logger.log("Crypto store already has sessions: not migrating");
return;
}
let numSessions = 0;
@@ -207,7 +197,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
);
}
}
console.log(
logger.log(
"Migrating " + numSessions + " sessions from session store",
);
});
@@ -236,14 +226,14 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
), txn,
);
} catch (e) {
console.warn(
logger.warn(
"Failed to migrate session " + s.senderKey + "/" +
s.sessionId + ": " + e.stack || e,
);
}
++numIbSessions;
}
console.log(
logger.log(
"Migrated " + numIbSessions +
" inbound group sessions from session store",
);
@@ -268,7 +258,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
*/
OlmDevice.prototype._getAccount = function(txn, func) {
this._cryptoStore.getAccount(txn, (pickledAccount) => {
const account = new Olm.Account();
const account = new global.Olm.Account();
try {
account.unpickle(this._pickleKey, pickledAccount);
func(account);
@@ -305,8 +295,8 @@ OlmDevice.prototype._storeAccount = function(txn, account) {
*/
OlmDevice.prototype._getSession = function(deviceKey, sessionId, txn, func) {
this._cryptoStore.getEndToEndSession(
deviceKey, sessionId, txn, (pickledSession) => {
this._unpickleSession(pickledSession, func);
deviceKey, sessionId, txn, (sessionInfo) => {
this._unpickleSession(sessionInfo, func);
},
);
};
@@ -316,15 +306,17 @@ OlmDevice.prototype._getSession = function(deviceKey, sessionId, txn, func) {
* function with it. The session object is destroyed once the function
* returns.
*
* @param {string} pickledSession
* @param {object} sessionInfo
* @param {function} func
* @private
*/
OlmDevice.prototype._unpickleSession = function(pickledSession, func) {
const session = new Olm.Session();
OlmDevice.prototype._unpickleSession = function(sessionInfo, func) {
const session = new global.Olm.Session();
try {
session.unpickle(this._pickleKey, pickledSession);
func(session);
session.unpickle(this._pickleKey, sessionInfo.session);
const unpickledSessInfo = Object.assign({}, sessionInfo, {session});
func(unpickledSessInfo);
} finally {
session.free();
}
@@ -334,14 +326,17 @@ OlmDevice.prototype._unpickleSession = function(pickledSession, func) {
* store our OlmSession in the session store
*
* @param {string} deviceKey
* @param {OlmSession} session
* @param {object} sessionInfo {session: OlmSession, lastReceivedMessageTs: int}
* @param {*} txn Opaque transaction object from cryptoStore.doTxn()
* @private
*/
OlmDevice.prototype._saveSession = function(deviceKey, session, txn) {
const pickledSession = session.pickle(this._pickleKey);
OlmDevice.prototype._saveSession = function(deviceKey, sessionInfo, txn) {
const sessionId = sessionInfo.session.session_id();
const pickledSessionInfo = Object.assign(sessionInfo, {
session: sessionInfo.session.pickle(this._pickleKey),
});
this._cryptoStore.storeEndToEndSession(
deviceKey, session.session_id(), pickledSession, txn,
deviceKey, sessionId, pickledSessionInfo, txn,
);
};
@@ -354,7 +349,7 @@ OlmDevice.prototype._saveSession = function(deviceKey, session, txn) {
* @private
*/
OlmDevice.prototype._getUtility = function(func) {
const utility = new Olm.Utility();
const utility = new global.Olm.Utility();
try {
return func(utility);
} finally {
@@ -466,12 +461,19 @@ OlmDevice.prototype.createOutboundSession = async function(
],
(txn) => {
this._getAccount(txn, (account) => {
const session = new Olm.Session();
const session = new global.Olm.Session();
try {
session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
newSessionId = session.session_id();
this._storeAccount(txn, account);
this._saveSession(theirIdentityKey, session, txn);
const sessionInfo = {
session,
// Pretend we've received a message at this point, otherwise
// if we try to send a message to the device, it won't use
// this session
lastReceivedMessageTs: Date.now(),
};
this._saveSession(theirIdentityKey, sessionInfo, txn);
} finally {
session.free();
}
@@ -510,7 +512,7 @@ OlmDevice.prototype.createInboundSession = async function(
],
(txn) => {
this._getAccount(txn, (account) => {
const session = new Olm.Session();
const session = new global.Olm.Session();
try {
session.create_inbound_from(
account, theirDeviceIdentityKey, ciphertext,
@@ -520,7 +522,13 @@ OlmDevice.prototype.createInboundSession = async function(
const payloadString = session.decrypt(messageType, ciphertext);
this._saveSession(theirDeviceIdentityKey, session, txn);
const sessionInfo = {
session,
// this counts as a received message: set last received message time
// to now
lastReceivedMessageTs: Date.now(),
};
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
result = {
payload: payloadString,
@@ -568,13 +576,30 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
* @return {Promise<?string>} session id, or null if no established session
*/
OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKey) {
const sessionIds = await this.getSessionIdsForDevice(theirDeviceIdentityKey);
if (sessionIds.length === 0) {
const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey);
if (sessionInfos.length === 0) {
return null;
}
// Use the session with the lowest ID.
sessionIds.sort();
return sessionIds[0];
// Use the session that has most recently received a message
let idxOfBest = 0;
for (let i = 1; i < sessionInfos.length; i++) {
const thisSessInfo = sessionInfos[i];
const thisLastReceived = thisSessInfo.lastReceivedMessageTs === undefined ?
0 : thisSessInfo.lastReceivedMessageTs;
const bestSessInfo = sessionInfos[idxOfBest];
const bestLastReceived = bestSessInfo.lastReceivedMessageTs === undefined ?
0 : bestSessInfo.lastReceivedMessageTs;
if (
thisLastReceived > bestLastReceived || (
thisLastReceived === bestLastReceived &&
thisSessInfo.sessionId < bestSessInfo.sessionId
)
) {
idxOfBest = i;
}
}
return sessionInfos[idxOfBest].sessionId;
};
/**
@@ -597,9 +622,10 @@ OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey)
this._cryptoStore.getEndToEndSessions(deviceIdentityKey, txn, (sessions) => {
const sessionIds = Object.keys(sessions).sort();
for (const sessionId of sessionIds) {
this._unpickleSession(sessions[sessionId], (session) => {
this._unpickleSession(sessions[sessionId], (sessInfo) => {
info.push({
hasReceivedMessage: session.has_received_message(),
lastReceivedMessageTs: sessInfo.lastReceivedMessageTs,
hasReceivedMessage: sessInfo.session.has_received_message(),
sessionId: sessionId,
});
});
@@ -630,9 +656,9 @@ OlmDevice.prototype.encryptMessage = async function(
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => {
res = session.encrypt(payloadString);
this._saveSession(theirDeviceIdentityKey, session, txn);
this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
res = sessionInfo.session.encrypt(payloadString);
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
});
},
);
@@ -657,9 +683,10 @@ OlmDevice.prototype.decryptMessage = async function(
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => {
payloadString = session.decrypt(messageType, ciphertext);
this._saveSession(theirDeviceIdentityKey, session, txn);
this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
payloadString = sessionInfo.session.decrypt(messageType, ciphertext);
sessionInfo.lastReceivedMessageTs = Date.now();
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
});
},
);
@@ -689,8 +716,8 @@ OlmDevice.prototype.matchesSession = async function(
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => {
matches = session.matches_inbound(ciphertext);
this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
matches = sessionInfo.session.matches_inbound(ciphertext);
});
},
);
@@ -724,11 +751,11 @@ OlmDevice.prototype._saveOutboundGroupSession = function(session) {
*/
OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
const pickled = this._outboundGroupSessionStore[sessionId];
if (pickled === null) {
if (pickled === undefined) {
throw new Error("Unknown outbound group session " + sessionId);
}
const session = new Olm.OutboundGroupSession();
const session = new global.Olm.OutboundGroupSession();
try {
session.unpickle(this._pickleKey, pickled);
return func(session);
@@ -744,7 +771,7 @@ OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
* @return {string} sessionId for the outbound session.
*/
OlmDevice.prototype.createOutboundGroupSession = function() {
const session = new Olm.OutboundGroupSession();
const session = new global.Olm.OutboundGroupSession();
try {
session.create();
this._saveOutboundGroupSession(session);
@@ -816,7 +843,7 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
* @return {*} result of func
*/
OlmDevice.prototype._unpickleInboundGroupSession = function(sessionData, func) {
const session = new Olm.InboundGroupSession();
const session = new global.Olm.InboundGroupSession();
try {
session.unpickle(this._pickleKey, sessionData.session);
return func(session);
@@ -888,16 +915,8 @@ OlmDevice.prototype.addInboundGroupSession = async function(
this._getInboundGroupSession(
roomId, senderKey, sessionId, txn,
(existingSession, existingSessionData) => {
if (existingSession) {
console.log(
"Update for megolm session " + senderKey + "/" + sessionId,
);
// for now we just ignore updates. TODO: implement something here
return;
}
// new session.
const session = new Olm.InboundGroupSession();
const session = new global.Olm.InboundGroupSession();
try {
if (exportFormat) {
session.import_session(sessionKey);
@@ -911,6 +930,20 @@ OlmDevice.prototype.addInboundGroupSession = async function(
);
}
if (existingSession) {
logger.log(
"Update for megolm session "
+ senderKey + "/" + sessionId,
);
if (existingSession.first_known_index()
<= session.first_known_index()) {
// existing session has lower index (i.e. can
// decrypt more), so keep it
logger.log("Keeping existing session");
return;
}
}
const sessionData = {
room_id: roomId,
session: session.pickle(this._pickleKey),
@@ -918,7 +951,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
};
this._cryptoStore.addEndToEndInboundGroupSession(
this._cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, sessionData, txn,
);
} finally {
@@ -1034,7 +1067,7 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
}
if (roomId !== sessionData.room_id) {
console.warn(
logger.warn(
`requested keys for inbound group session ${senderKey}|` +
`${sessionId}, with incorrect room_id ` +
`(expected ${sessionData.room_id}, ` +
@@ -1058,6 +1091,8 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
* @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 {integer} chainIndex The chain index at which to export the session.
* If omitted, export at the first index we know about.
*
* @returns {Promise<{chain_index: number, key: string,
* forwarding_curve25519_key_chain: Array<string>,
@@ -1065,9 +1100,12 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
* }>}
* details of the session key. The key is a base64-encoded megolm key in
* export format.
*
* @throws Error If the given chain index could not be obtained from the known
* index (ie. the given chain index is before the first we have).
*/
OlmDevice.prototype.getInboundGroupSessionKey = async function(
roomId, senderKey, sessionId,
roomId, senderKey, sessionId, chainIndex,
) {
let result;
await this._cryptoStore.doTxn(
@@ -1078,14 +1116,19 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
result = null;
return;
}
const messageIndex = session.first_known_index();
if (chainIndex === undefined) {
chainIndex = session.first_known_index();
}
const exportedSession = session.export_session(chainIndex);
const claimedKeys = sessionData.keysClaimed || {};
const senderEd25519Key = claimedKeys.ed25519 || null;
result = {
"chain_index": messageIndex,
"key": session.export_session(messageIndex),
"chain_index": chainIndex,
"key": exportedSession,
"forwarding_curve25519_key_chain":
sessionData.forwardingCurve25519KeyChain || [],
"sender_claimed_ed25519_key": senderEd25519Key,
@@ -1119,6 +1162,7 @@ OlmDevice.prototype.exportInboundGroupSession = function(
"session_id": sessionId,
"session_key": session.export_session(messageIndex),
"forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [],
"first_known_index": session.first_known_index(),
};
});
};
+126 -40
View File
@@ -16,6 +16,7 @@ limitations under the License.
import Promise from 'bluebird';
import logger from '../logger';
import utils from '../utils';
/**
@@ -108,7 +109,7 @@ export default class OutgoingRoomKeyRequestManager {
* Called when the client is stopped. Stops any running background processes.
*/
stop() {
console.log('stopping OutgoingRoomKeyRequestManager');
logger.log('stopping OutgoingRoomKeyRequestManager');
// stop the timer on the next run
this._clientRunning = false;
}
@@ -123,35 +124,118 @@ export default class OutgoingRoomKeyRequestManager {
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* @param {Array<{userId: string, deviceId: string}>} recipients
* @param {boolean} resend whether to resend the key request if there is
* already one
*
* @returns {Promise} resolves when the request has been added to the
* pending list (or we have established that a similar request already
* exists)
*/
sendRoomKeyRequest(requestBody, recipients) {
return this._cryptoStore.getOrAddOutgoingRoomKeyRequest({
requestBody: requestBody,
recipients: recipients,
requestId: this._baseApis.makeTxnId(),
state: ROOM_KEY_REQUEST_STATES.UNSENT,
}).then((req) => {
if (req.state === ROOM_KEY_REQUEST_STATES.UNSENT) {
this._startTimer();
async sendRoomKeyRequest(requestBody, recipients, resend=false) {
const req = await this._cryptoStore.getOutgoingRoomKeyRequest(
requestBody,
);
if (!req) {
await this._cryptoStore.getOrAddOutgoingRoomKeyRequest({
requestBody: requestBody,
recipients: recipients,
requestId: this._baseApis.makeTxnId(),
state: ROOM_KEY_REQUEST_STATES.UNSENT,
});
} else {
switch (req.state) {
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND:
case ROOM_KEY_REQUEST_STATES.UNSENT:
// nothing to do here, since we're going to send a request anyways
return;
case ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING: {
// existing request is about to be cancelled. If we want to
// resend, then change the state so that it resends after
// cancelling. Otherwise, just cancel the cancellation.
const state = resend ?
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND :
ROOM_KEY_REQUEST_STATES.SENT;
await this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING, {
state,
cancellationTxnId: this._baseApis.makeTxnId(),
},
);
break;
}
});
case ROOM_KEY_REQUEST_STATES.SENT: {
// a request has already been sent. If we don't want to
// resend, then do nothing. If we do want to, then cancel the
// existing request and send a new one.
if (resend) {
const state =
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND;
const updatedReq =
await this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.SENT, {
state,
cancellationTxnId: this._baseApis.makeTxnId(),
// need to use a new transaction ID so that
// the request gets sent
requestTxnId: this._baseApis.makeTxnId(),
},
);
if (!updatedReq) {
// updateOutgoingRoomKeyRequest couldn't find the request
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
// raced with another tab to mark the request cancelled.
// Try again, to make sure the request is resent.
return await this.sendRoomKeyRequest(
requestBody, recipients, resend,
);
}
// We don't want to wait for the timer, so we send it
// immediately. (We might actually end up racing with the timer,
// but that's ok: even if we make the request twice, we'll do it
// with the same transaction_id, so only one message will get
// sent).
//
// (We also don't want to wait for the response from the server
// here, as it will slow down processing of received keys if we
// do.)
try {
await this._sendOutgoingRoomKeyRequestCancellation(
updatedReq,
true,
);
} catch (e) {
logger.error(
"Error sending room key request cancellation;"
+ " will retry later.", e,
);
}
// The request has transitioned from
// CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We
// still need to resend the request which is now UNSENT, so
// start the timer if it isn't already started.
}
break;
}
default:
throw new Error('unhandled state: ' + req.state);
}
}
// some of the branches require the timer to be started. Just start it
// all the time, because it doesn't hurt to start it.
this._startTimer();
}
/**
* Cancel room key requests, if any match the given requestBody
*
* @param {module:crypto~RoomKeyRequestBody} requestBody
* @param {boolean} andResend if true, transition to UNSENT instead of
* deleting after sending cancellation.
*
* @returns {Promise} resolves when the request has been updated in our
* pending list.
*/
cancelRoomKeyRequest(requestBody, andResend=false) {
cancelRoomKeyRequest(requestBody) {
return this._cryptoStore.getOutgoingRoomKeyRequest(
requestBody,
).then((req) => {
@@ -173,7 +257,7 @@ export default class OutgoingRoomKeyRequestManager {
// may have seen it, so we still need to send a cancellation
// in that case :/
console.log(
logger.log(
'deleting unnecessary room key request for ' +
stringifyRequestBody(requestBody),
);
@@ -182,15 +266,10 @@ export default class OutgoingRoomKeyRequestManager {
);
case ROOM_KEY_REQUEST_STATES.SENT: {
// If `andResend` is set, transition to UNSENT once the
// cancellation has successfully been sent.
const state = andResend ?
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND :
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING;
// send a cancellation.
return this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.SENT, {
state,
state: ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
cancellationTxnId: this._baseApis.makeTxnId(),
},
).then((updatedReq) => {
@@ -201,7 +280,7 @@ export default class OutgoingRoomKeyRequestManager {
// the request cancelled. There is no point in
// sending another cancellation since the other tab
// will do it.
console.log(
logger.log(
'Tried to cancel room key request for ' +
stringifyRequestBody(requestBody) +
' but it was already cancelled in another tab',
@@ -220,20 +299,12 @@ export default class OutgoingRoomKeyRequestManager {
// do.)
this._sendOutgoingRoomKeyRequestCancellation(
updatedReq,
andResend,
).catch((e) => {
console.error(
logger.error(
"Error sending room key request cancellation;"
+ " will retry later.", e,
);
this._startTimer();
}).then(() => {
if (!andResend) return;
// The request has transitioned from
// CANCELLATION_PENDING_AND_WILL_RESEND to UNSENT. We
// still need to resend the request which is now UNSENT, so
// start the timer if it isn't already started.
this._startTimer();
});
});
}
@@ -243,6 +314,21 @@ export default class OutgoingRoomKeyRequestManager {
});
}
/**
* Look for room key requests by target device and state
*
* @param {string} userId Target user ID
* @param {string} deviceId Target device ID
*
* @return {Promise} resolves to a list of all the
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
*/
getOutgoingSentRoomKeyRequest(userId, deviceId) {
return this._cryptoStore.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, [ROOM_KEY_REQUEST_STATES.SENT],
);
}
// start the background timer to send queued requests, if the timer isn't
// already running
_startTimer() {
@@ -261,10 +347,10 @@ export default class OutgoingRoomKeyRequestManager {
}).catch((e) => {
// this should only happen if there is an indexeddb error,
// in which case we're a bit stuffed anyway.
console.warn(
logger.warn(
`error in OutgoingRoomKeyRequestManager: ${e}`,
);
}).done();
});
};
this._sendOutgoingRoomKeyRequestsTimer = global.setTimeout(
@@ -282,7 +368,7 @@ export default class OutgoingRoomKeyRequestManager {
return Promise.resolve();
}
console.log("Looking for queued outgoing room key requests");
logger.log("Looking for queued outgoing room key requests");
return this._cryptoStore.getOutgoingRoomKeyRequestByState([
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
@@ -290,7 +376,7 @@ export default class OutgoingRoomKeyRequestManager {
ROOM_KEY_REQUEST_STATES.UNSENT,
]).then((req) => {
if (!req) {
console.log("No more outgoing room key requests");
logger.log("No more outgoing room key requests");
this._sendOutgoingRoomKeyRequestsTimer = null;
return;
}
@@ -312,16 +398,16 @@ export default class OutgoingRoomKeyRequestManager {
// go around the loop again
return this._sendOutgoingRoomKeyRequests();
}).catch((e) => {
console.error("Error sending room key request; will retry later.", e);
logger.error("Error sending room key request; will retry later.", e);
this._sendOutgoingRoomKeyRequestsTimer = null;
this._startTimer();
}).done();
});
});
}
// given a RoomKeyRequest, send it and update the request record
_sendOutgoingRoomKeyRequest(req) {
console.log(
logger.log(
`Requesting keys for ${stringifyRequestBody(req.requestBody)}` +
` from ${stringifyRecipientList(req.recipients)}` +
`(id ${req.requestId})`,
@@ -335,7 +421,7 @@ export default class OutgoingRoomKeyRequestManager {
};
return this._sendMessageToDevices(
requestMessage, req.recipients, req.requestId,
requestMessage, req.recipients, req.requestTxnId || req.requestId,
).then(() => {
return this._cryptoStore.updateOutgoingRoomKeyRequest(
req.requestId, ROOM_KEY_REQUEST_STATES.UNSENT,
@@ -347,7 +433,7 @@ export default class OutgoingRoomKeyRequestManager {
// Given a RoomKeyRequest, cancel it and delete the request record unless
// andResend is set, in which case transition to UNSENT.
_sendOutgoingRoomKeyRequestCancellation(req, andResend) {
console.log(
logger.log(
`Sending cancellation for key request for ` +
`${stringifyRequestBody(req.requestBody)} to ` +
`${stringifyRecipientList(req.recipients)} ` +
+3
View File
@@ -71,6 +71,9 @@ export default class RoomList {
}
async setRoomEncryption(roomId, roomInfo) {
// important that this happens before calling into the store
// as it prevents the Crypto::setRoomEncryption from calling
// this twice for consecutive m.room.encryption events
this._roomEncryption[roomId] = roomInfo;
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
+2 -1
View File
@@ -176,8 +176,9 @@ export {DecryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
* @extends Error
*/
class DecryptionError extends Error {
constructor(msg, details) {
constructor(code, msg, details) {
super(msg);
this.code = code;
this.name = 'DecryptionError';
this.detailedString = _detailedStringForDecryptionError(this, details);
}
+235 -72
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -23,6 +24,7 @@ limitations under the License.
import Promise from 'bluebird';
const logger = require("../../logger");
const utils = require("../../utils");
const olmlib = require("../olmlib");
const base = require("./base");
@@ -64,7 +66,7 @@ OutboundSessionInfo.prototype.needsRotation = function(
if (this.useCount >= rotationPeriodMsgs ||
sessionLifetime >= rotationPeriodMs
) {
console.log(
logger.log(
"Rotating megolm session after " + this.useCount +
" messages, " + sessionLifetime + "ms",
);
@@ -102,7 +104,7 @@ OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
}
if (!devicesInRoom.hasOwnProperty(userId)) {
console.log("Starting new session because we shared with " + userId);
logger.log("Starting new session because we shared with " + userId);
return true;
}
@@ -112,7 +114,7 @@ OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
}
if (!devicesInRoom[userId].hasOwnProperty(deviceId)) {
console.log(
logger.log(
"Starting new session because we shared with " +
userId + ":" + deviceId,
);
@@ -142,6 +144,11 @@ function MegolmEncryption(params) {
// room).
this._setupPromise = Promise.resolve();
// Map of outbound sessions by sessions ID. Used if we need a particular
// session (the session we're currently using to send is always obtained
// using _setupPromise).
this._outboundSessions = {};
// default rotation periods
this._sessionRotationPeriodMsgs = 100;
this._sessionRotationPeriodMs = 7 * 24 * 3600 * 1000;
@@ -181,7 +188,7 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
if (session && session.needsRotation(self._sessionRotationPeriodMsgs,
self._sessionRotationPeriodMs)
) {
console.log("Starting new megolm session because we need to rotate.");
logger.log("Starting new megolm session because we need to rotate.");
session = null;
}
@@ -191,8 +198,9 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
}
if (!session) {
console.log(`Starting new megolm session for room ${self._roomId}`);
logger.log(`Starting new megolm session for room ${self._roomId}`);
session = await self._prepareNewSession();
self._outboundSessions[session.sessionId] = session;
}
// now check if we need to share with any devices
@@ -262,6 +270,18 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
key.key, {ed25519: this._olmDevice.deviceEd25519Key},
);
if (this._crypto.backupInfo) {
// don't wait for it to complete
this._crypto.backupGroupSession(
this._roomId, this._olmDevice.deviceCurve25519Key, [],
sessionId, key.key,
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
console.log("Failed to back up group session", e);
});
}
return new OutboundSessionInfo(sessionId);
};
@@ -318,7 +338,7 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
continue;
}
console.log(
logger.log(
"share keys with device " + userId + ":" + deviceId,
);
@@ -407,8 +427,98 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
};
/**
* @private
* Re-shares a megolm session key with devices if the key has already been
* sent to them.
*
* @param {string} senderKey The key of the originating device for the session
* @param {string} sessionId ID of the outbound session to share
* @param {string} userId ID of the user who owns the target device
* @param {module:crypto/deviceinfo} device The target device
*/
MegolmEncryption.prototype.reshareKeyWithDevice = async function(
senderKey, sessionId, userId, device,
) {
const obSessionInfo = this._outboundSessions[sessionId];
if (!obSessionInfo) {
logger.debug("Session ID " + sessionId + " not found: not re-sharing keys");
return;
}
// The chain index of the key we previously sent this device
if (obSessionInfo.sharedWithDevices[userId] === undefined) {
logger.debug("Session ID " + sessionId + " never shared with user " + userId);
return;
}
const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId];
if (sentChainIndex === undefined) {
logger.debug(
"Session ID " + sessionId + " never shared with device " +
userId + ":" + device.deviceId,
);
return;
}
// get the key from the inbound session: the outbound one will already
// have been ratcheted to the next chain index.
const key = await this._olmDevice.getInboundGroupSessionKey(
this._roomId, senderKey, sessionId, sentChainIndex,
);
if (!key) {
logger.warn(
"No outbound session key found for " + sessionId + ": not re-sharing keys",
);
return;
}
await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, {
[userId]: {
[device.deviceId]: device,
},
},
);
const payload = {
type: "m.forwarded_room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: sessionId,
session_key: key.key,
chain_index: key.chain_index,
sender_key: senderKey,
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain,
},
};
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this._userId,
this._deviceId,
this._olmDevice,
userId,
device,
payload,
),
await this._baseApis.sendToDevice("m.room.encrypted", {
[userId]: {
[device.deviceId]: encryptedContent,
},
});
logger.debug(
`Re-shared key for session ${sessionId} with ${userId}:${device.deviceId}`,
);
};
/**
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
@@ -440,10 +550,10 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
await this._encryptAndSendKeysToDevices(
session, key.chain_index, userDeviceMaps[i], payload,
);
console.log(`Completed megolm keyshare in ${this._roomId} `
logger.log(`Completed megolm keyshare in ${this._roomId} `
+ `(slice ${i + 1}/${userDeviceMaps.length})`);
} catch (e) {
console.log(`megolm keyshare in ${this._roomId} `
logger.log(`megolm keyshare in ${this._roomId} `
+ `(slice ${i + 1}/${userDeviceMaps.length}) failed`);
throw e;
@@ -462,7 +572,7 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
*/
MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
const self = this;
console.log(`Starting to encrypt event for ${this._roomId}`);
logger.log(`Starting to encrypt event for ${this._roomId}`);
return this._getDevicesInRoom(room).then(function(devicesInRoom) {
// check if any of these devices are not yet known to the user.
@@ -488,6 +598,8 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
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.
// XXX: Do we still need this now that m.new_device messages
// no longer exist since #483?
device_id: self._deviceId,
};
@@ -496,6 +608,16 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
});
};
/**
* Forces the current outbound group session to be discarded such
* that another one will be created next time an event is sent.
*
* This should not normally be necessary.
*/
MegolmEncryption.prototype.forceDiscardSession = function() {
this._setupPromise = this._setupPromise.then(() => null);
};
/**
* 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
@@ -535,9 +657,9 @@ MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) {
* @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) {
MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
const members = await room.getEncryptionTargetMembers();
const roomMembers = utils.map(members, function(u) {
return u.userId;
});
@@ -550,35 +672,31 @@ MegolmEncryption.prototype._getDevicesInRoom = function(room) {
// 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)) {
// device_lists in their /sync response. This cache should then be maintained
// using all the device_lists changes and left fields.
// See https://github.com/vector-im/riot-web/issues/2305 for details.
const devices = await this._crypto.downloadKeys(roomMembers, false);
// 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;
}
const userDevices = devices[userId];
for (const deviceId in userDevices) {
if (!userDevices.hasOwnProperty(deviceId)) {
continue;
}
if (userDevices[deviceId].isBlocked() ||
(userDevices[deviceId].isUnverified() && isBlacklisting)
) {
delete userDevices[deviceId];
}
if (userDevices[deviceId].isBlocked() ||
(userDevices[deviceId].isUnverified() && isBlacklisting)
) {
delete userDevices[deviceId];
}
}
}
return devices;
});
return devices;
};
/**
@@ -618,7 +736,10 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
if (!content.sender_key || !content.session_id ||
!content.ciphertext
) {
throw new base.DecryptionError("Missing fields in input");
throw new base.DecryptionError(
"MEGOLM_MISSING_FIELDS",
"Missing fields in input",
);
}
// we add the event to the pending list *before* we start decryption.
@@ -635,11 +756,17 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
event.getId(), event.getTs(),
);
} catch (e) {
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR";
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
this._requestKeysForEvent(event);
errorCode = 'OLM_UNKNOWN_MESSAGE_INDEX';
}
throw new base.DecryptionError(
e.toString(), {
errorCode,
e ? e.toString() : "Unknown Error: Error is undefined", {
session: content.sender_key + '|' + content.session_id,
},
);
@@ -655,6 +782,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
// scheduled, so we needn't send out the request here.)
this._requestKeysForEvent(event);
throw new base.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
"The sender's device has not sent us the keys for this message.",
{
session: content.sender_key + '|' + content.session_id,
@@ -673,6 +801,7 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
// room, so neither the sender nor a MITM can lie about the room_id).
if (payload.room_id !== event.getRoomId()) {
throw new base.DecryptionError(
"MEGOLM_BAD_ROOM",
"Message intended for room " + payload.room_id,
);
}
@@ -686,19 +815,9 @@ MegolmDecryption.prototype.decryptEvent = async function(event) {
};
MegolmDecryption.prototype._requestKeysForEvent = function(event) {
const sender = event.getSender();
const wireContent = event.getWireContent();
// send the request to all of our own devices, and the
// original sending device if it wasn't us.
const recipients = [{
userId: this._userId, deviceId: '*',
}];
if (sender != this._userId) {
recipients.push({
userId: sender, deviceId: wireContent.device_id,
});
}
const recipients = event.getKeyRequestRecipients(this._userId);
this._crypto.requestRoomKey({
room_id: event.getRoomId(),
@@ -762,12 +881,12 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
!sessionId ||
!content.session_key
) {
console.error("key event is missing fields");
logger.error("key event is missing fields");
return;
}
if (!senderKey) {
console.error("key event has no sender key (not encrypted?)");
logger.error("key event has no sender key (not encrypted?)");
return;
}
@@ -784,13 +903,13 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
senderKey = content.sender_key;
if (!senderKey) {
console.error("forwarded_room_key event is missing sender_key field");
logger.error("forwarded_room_key event is missing sender_key field");
return;
}
const ed25519Key = content.sender_claimed_ed25519_key;
if (!ed25519Key) {
console.error(
logger.error(
`forwarded_room_key_event is missing sender_claimed_ed25519_key field`,
);
return;
@@ -803,24 +922,44 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
keysClaimed = event.getKeysClaimed();
}
console.log(`Adding key for megolm session ${senderKey}|${sessionId}`);
this._olmDevice.addInboundGroupSession(
logger.log(`Adding key for megolm session ${senderKey}|${sessionId}`);
return this._olmDevice.addInboundGroupSession(
content.room_id, senderKey, forwardingKeyChain, sessionId,
content.session_key, keysClaimed,
exportFormat,
).then(() => {
// cancel any outstanding room key requests for this session
this._crypto.cancelRoomKeyRequest({
algorithm: content.algorithm,
room_id: content.room_id,
session_id: content.session_id,
sender_key: senderKey,
});
// have another go at decrypting events sent with this session.
this._retryDecryption(senderKey, sessionId);
this._retryDecryption(senderKey, sessionId)
.then((success) => {
// cancel any outstanding room key requests for this session.
// Only do this if we managed to decrypt every message in the
// session, because if we didn't, we leave the other key
// requests in the hopes that someone sends us a key that
// includes an earlier index.
if (success) {
this._crypto.cancelRoomKeyRequest({
algorithm: content.algorithm,
room_id: content.room_id,
session_id: content.session_id,
sender_key: senderKey,
});
}
});
}).then(() => {
if (this._crypto.backupInfo) {
// don't wait for the keys to be backed up for the server
this._crypto.backupGroupSession(
content.room_id, senderKey, forwardingKeyChain,
content.session_id, content.session_key, keysClaimed,
exportFormat,
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
console.log("Failed to back up group session", e);
});
}
}).catch((e) => {
console.error(`Error handling m.room_key_event: ${e}`);
logger.error(`Error handling m.room_key_event: ${e}`);
});
};
@@ -862,7 +1001,7 @@ MegolmDecryption.prototype.shareKeysWithDevice = function(keyRequest) {
return null;
}
console.log(
logger.log(
"sharing keys for session " + body.sender_key + "|"
+ body.session_id + " with device "
+ userId + ":" + deviceId,
@@ -936,6 +1075,22 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
session.sender_claimed_keys,
true,
).then(() => {
if (this._crypto.backupInfo) {
// don't wait for it to complete
this._crypto.backupGroupSession(
session.room_id,
session.sender_key,
session.forwarding_curve25519_key_chain,
session.session_id,
session.session_key,
session.sender_claimed_keys,
true,
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
console.log("Failed to back up group session", e);
});
}
// have another go at decrypting events sent with this session.
this._retryDecryption(session.sender_key, session.session_id);
});
@@ -947,19 +1102,27 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
* @private
* @param {String} senderKey
* @param {String} sessionId
*
* @return {Boolean} whether all messages were successfully decrypted
*/
MegolmDecryption.prototype._retryDecryption = function(senderKey, sessionId) {
MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionId) {
const k = senderKey + "|" + sessionId;
const pending = this._pendingEvents[k];
if (!pending) {
return;
return true;
}
delete this._pendingEvents[k];
for (const ev of pending) {
ev.attemptDecryption(this._crypto);
}
await Promise.all([...pending].map(async (ev) => {
try {
await ev.attemptDecryption(this._crypto);
} catch (e) {
// don't die if something goes wrong
}
}));
return !this._pendingEvents[k];
};
base.registerAlgorithm(
+56 -42
View File
@@ -22,6 +22,7 @@ limitations under the License.
*/
import Promise from 'bluebird';
const logger = require("../../logger");
const utils = require("../../utils");
const olmlib = require("../olmlib");
const DeviceInfo = require("../deviceinfo");
@@ -83,60 +84,62 @@ OlmEncryption.prototype._ensureSession = function(roomMembers) {
*
* @return {module:client.Promise} Promise which resolves to the new event body
*/
OlmEncryption.prototype.encryptMessage = function(room, eventType, content) {
OlmEncryption.prototype.encryptMessage = async 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) {
const members = await room.getEncryptionTargetMembers();
const users = utils.map(members, function(u) {
return u.userId;
});
const self = this;
return this._ensureSession(users).then(function() {
const payloadFields = {
room_id: room.roomId,
type: eventType,
content: content,
};
await this._ensureSession(users);
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
const payloadFields = {
room_id: room.roomId,
type: eventType,
content: content,
};
const promises = [];
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);
const promises = [];
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;
}
for (let i = 0; i < users.length; ++i) {
const userId = users[i];
const devices = self._crypto.getStoredDevicesForUser(userId);
promises.push(
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
self._userId, self._deviceId, self._olmDevice,
userId, deviceInfo, payloadFields,
),
);
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;
}
}
return Promise.all(promises).return(encryptedContent);
});
promises.push(
olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
self._userId, self._deviceId, self._olmDevice,
userId, deviceInfo, payloadFields,
),
);
}
}
return await Promise.all(promises).return(encryptedContent);
};
/**
@@ -168,11 +171,17 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
const ciphertext = content.ciphertext;
if (!ciphertext) {
throw new base.DecryptionError("Missing ciphertext");
throw new base.DecryptionError(
"OLM_MISSING_CIPHERTEXT",
"Missing ciphertext",
);
}
if (!(this._olmDevice.deviceCurve25519Key in ciphertext)) {
throw new base.DecryptionError("Not included in recipients");
throw new base.DecryptionError(
"OLM_NOT_INCLUDED_IN_RECIPIENTS",
"Not included in recipients",
);
}
const message = ciphertext[this._olmDevice.deviceCurve25519Key];
let payloadString;
@@ -181,6 +190,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
payloadString = await this._decryptMessage(deviceKey, message);
} catch (e) {
throw new base.DecryptionError(
"OLM_BAD_ENCRYPTED_MESSAGE",
"Bad Encrypted Message", {
sender: deviceKey,
err: e,
@@ -194,12 +204,14 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
// https://github.com/vector-im/vector-web/issues/2483
if (payload.recipient != this._userId) {
throw new base.DecryptionError(
"OLM_BAD_RECIPIENT",
"Message was intented for " + payload.recipient,
);
}
if (payload.recipient_keys.ed25519 != this._olmDevice.deviceEd25519Key) {
throw new base.DecryptionError(
"OLM_BAD_RECIPIENT_KEY",
"Message not intended for this device", {
intended: payload.recipient_keys.ed25519,
our_key: this._olmDevice.deviceEd25519Key,
@@ -213,6 +225,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
// which is checked elsewhere).
if (payload.sender != event.getSender()) {
throw new base.DecryptionError(
"OLM_FORWARDED_MESSAGE",
"Message forwarded from " + payload.sender, {
reported_sender: event.getSender(),
},
@@ -222,6 +235,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
// Olm events intended for a room have a room_id.
if (payload.room_id !== event.getRoomId()) {
throw new base.DecryptionError(
"OLM_BAD_ROOM",
"Message intended for room " + payload.room_id, {
reported_room: event.room_id,
},
@@ -260,7 +274,7 @@ OlmDecryption.prototype._decryptMessage = async function(
const payload = await this._olmDevice.decryptMessage(
theirDeviceIdentityKey, sessionId, message.type, message.body,
);
console.log(
logger.log(
"Decrypted Olm message from " + theirDeviceIdentityKey +
" with session " + sessionId,
);
@@ -315,7 +329,7 @@ OlmDecryption.prototype._decryptMessage = async function(
);
}
console.log(
logger.log(
"created new inbound Olm session ID " +
res.session_id + " with " + theirDeviceIdentityKey,
);
+81
View File
@@ -0,0 +1,81 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { randomString } from '../randomstring';
const DEFAULT_ITERATIONS = 500000;
export async function keyForExistingBackup(backupData, password) {
if (!global.Olm) {
throw new Error("Olm is not available");
}
const authData = backupData.auth_data;
if (!authData.private_key_salt || !authData.private_key_iterations) {
throw new Error(
"Salt and/or iterations not found: " +
"this backup cannot be restored with a passphrase",
);
}
return await deriveKey(
password, backupData.auth_data.private_key_salt,
backupData.auth_data.private_key_iterations,
);
}
export async function keyForNewBackup(password) {
if (!global.Olm) {
throw new Error("Olm is not available");
}
const salt = randomString(32);
const key = await deriveKey(password, salt, DEFAULT_ITERATIONS);
return { key, salt, iterations: DEFAULT_ITERATIONS };
}
async function deriveKey(password, salt, iterations) {
const subtleCrypto = global.crypto.subtle;
const TextEncoder = global.TextEncoder;
if (!subtleCrypto || !TextEncoder) {
// TODO: Implement this for node
throw new Error("Password-based backup is not avaiable on this platform");
}
const key = await subtleCrypto.importKey(
'raw',
new TextEncoder().encode(password),
{name: 'PBKDF2'},
false,
['deriveBits'],
);
const keybits = await subtleCrypto.deriveBits(
{
name: 'PBKDF2',
salt: new TextEncoder().encode(salt),
iterations: iterations,
hash: 'SHA-512',
},
key,
global.Olm.PRIVATE_KEY_LENGTH * 8,
);
return new Uint8Array(keybits);
}
+1048 -89
View File
File diff suppressed because it is too large Load Diff
+18 -9
View File
@@ -23,6 +23,7 @@ limitations under the License.
import Promise from 'bluebird';
const anotherjson = require('another-json');
const logger = require("../logger");
const utils = require("../utils");
/**
@@ -35,6 +36,11 @@ module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
*/
module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
/**
* matrix algorithm tag for megolm backups
*/
module.exports.MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2";
/**
* Encrypt an event payload for an Olm device
@@ -65,7 +71,7 @@ module.exports.encryptMessageForDevice = async function(
return;
}
console.log(
logger.log(
"Using sessionid " + sessionId + " for device " +
recipientUserId + ":" + recipientDevice.deviceId,
);
@@ -115,14 +121,17 @@ module.exports.encryptMessageForDevice = async function(
* @param {module:base-apis~MatrixBaseApis} baseApis
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
* map from userid to list of devices to ensure sessions for
*
* @param {bolean} force If true, establish a new session even if one already exists.
* Optional.
*
* @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 = async function(
olmDevice, baseApis, devicesByUser,
olmDevice, baseApis, devicesByUser, force,
) {
const devicesWithoutSession = [
// [userId, deviceId], ...
@@ -140,7 +149,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
const sessionId = await olmDevice.getSessionIdForDevice(key);
if (sessionId === null) {
if (sessionId === null || force) {
devicesWithoutSession.push([userId, deviceId]);
}
result[userId][deviceId] = {
@@ -176,7 +185,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
if (result[userId][deviceId].sessionId) {
if (result[userId][deviceId].sessionId && !force) {
// we already have a result for this device
continue;
}
@@ -190,7 +199,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
}
if (!oneTimeKey) {
console.warn(
logger.warn(
"No one-time keys (alg=" + oneTimeKeyAlgorithm +
") for device " + userId + ":" + deviceId,
);
@@ -219,7 +228,7 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
deviceInfo.getFingerprint(),
);
} catch (e) {
console.error(
logger.error(
"Unable to verify signature on one-time key for device " +
userId + ":" + deviceId + ":", e,
);
@@ -233,12 +242,12 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
);
} catch (e) {
// possibly a bad key
console.error("Error starting session with device " +
logger.error("Error starting session with device " +
userId + ":" + deviceId + ": " + e);
return null;
}
console.log("Started new sessionid " + sid +
logger.log("Started new sessionid " + sid +
" for device " + userId + ":" + deviceId);
return sid;
}
+66
View File
@@ -0,0 +1,66 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import bs58 from 'bs58';
// picked arbitrarily but to try & avoid clashing with any bitcoin ones
// (which are also base58 encoded, but bitcoin's involve a lot more hashing)
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
export function encodeRecoveryKey(key) {
const buf = new Uint8Array(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
let parity = 0;
for (let i = 0; i < buf.length - 1; ++i) {
parity ^= buf[i];
}
buf[buf.length - 1] = parity;
const base58key = bs58.encode(buf);
return base58key.match(/.{1,4}/g).join(" ");
}
export function decodeRecoveryKey(recoverykey) {
const result = bs58.decode(recoverykey.replace(/ /g, ''));
let parity = 0;
for (const b of result) {
parity ^= b;
}
if (parity !== 0) {
throw new Error("Incorrect parity");
}
for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) {
if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) {
throw new Error("Incorrect prefix");
}
}
if (
result.length !==
OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1
) {
throw new Error("Incorrect length");
}
return result.slice(
OLM_RECOVERY_KEY_PREFIX.length,
OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH,
);
}
@@ -16,9 +16,11 @@ limitations under the License.
*/
import Promise from 'bluebird';
import logger from '../../logger';
import utils from '../../utils';
export const VERSION = 6;
export const VERSION = 7;
/**
* Implementation of a CryptoStore which is backed by an existing
@@ -38,7 +40,7 @@ export class Backend {
// attempts to delete the database will block (and subsequent
// attempts to re-create it will also block).
db.onversionchange = (ev) => {
console.log(`versionchange for indexeddb ${this._dbName}: closing`);
logger.log(`versionchange for indexeddb ${this._dbName}: closing`);
db.close();
};
}
@@ -64,7 +66,7 @@ export class Backend {
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
if (existing) {
// this entry matches the request - return it.
console.log(
logger.log(
`already have key request outstanding for ` +
`${requestBody.room_id} / ${requestBody.session_id}: ` +
`not sending another`,
@@ -75,7 +77,7 @@ export class Backend {
// we got to the end of the list without finding a match
// - add the new request.
console.log(
logger.log(
`enqueueing key request for ${requestBody.room_id} / ` +
requestBody.session_id,
);
@@ -204,6 +206,42 @@ export class Backend {
return promiseifyTxn(txn).then(() => result);
}
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
let stateIndex = 0;
const results = [];
function onsuccess(ev) {
const cursor = ev.target.result;
if (cursor) {
const keyReq = cursor.value;
if (keyReq.recipients.includes({userId, deviceId})) {
results.push(keyReq);
}
cursor.continue();
} else {
// try the next state in the list
stateIndex++;
if (stateIndex >= wantedStates.length) {
// no matches
return;
}
const wantedState = wantedStates[stateIndex];
const cursorReq = ev.target.source.openCursor(wantedState);
cursorReq.onsuccess = onsuccess;
}
}
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
const store = txn.objectStore("outgoingRoomKeyRequests");
const wantedState = wantedStates[stateIndex];
const cursorReq = store.index("state").openCursor(wantedState);
cursorReq.onsuccess = onsuccess;
return promiseifyTxn(txn).then(() => results);
}
/**
* Look for an existing room key request by id and state, and update it if
* found
@@ -226,7 +264,7 @@ export class Backend {
}
const data = cursor.value;
if (data.state != expectedState) {
console.warn(
logger.warn(
`Cannot update room key request from ${expectedState} ` +
`as it was already updated to ${data.state}`,
);
@@ -264,7 +302,7 @@ export class Backend {
}
const data = cursor.value;
if (data.state != expectedState) {
console.warn(
logger.warn(
`Cannot delete room key request in state ${data.state} `
+ `(expected ${expectedState})`,
);
@@ -312,7 +350,10 @@ export class Backend {
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
results[cursor.value.sessionId] = cursor.value.session;
results[cursor.value.sessionId] = {
session: cursor.value.session,
lastReceivedMessageTs: cursor.value.lastReceivedMessageTs,
};
cursor.continue();
} else {
try {
@@ -330,7 +371,10 @@ export class Backend {
getReq.onsuccess = function() {
try {
if (getReq.result) {
func(getReq.result.session);
func({
session: getReq.result.session,
lastReceivedMessageTs: getReq.result.lastReceivedMessageTs,
});
} else {
func(null);
}
@@ -340,9 +384,32 @@ export class Backend {
};
}
storeEndToEndSession(deviceKey, sessionId, session, txn) {
getAllEndToEndSessions(txn, func) {
const objectStore = txn.objectStore("sessions");
objectStore.put({deviceKey, sessionId, session});
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
func(cursor.value);
cursor.continue();
} else {
try {
func(null);
} catch (e) {
abortWithException(txn, e);
}
}
};
}
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
const objectStore = txn.objectStore("sessions");
objectStore.put({
deviceKey,
sessionId,
session: sessionInfo.session,
lastReceivedMessageTs: sessionInfo.lastReceivedMessageTs,
});
}
// Inbound group sessions
@@ -400,7 +467,7 @@ export class Backend {
ev.stopPropagation();
// ...and this stops it from aborting the transaction
ev.preventDefault();
console.log(
logger.log(
"Ignoring duplicate inbound group session: " +
senderCurve25519Key + " / " + sessionId,
);
@@ -460,6 +527,85 @@ export class Backend {
};
}
// session backups
getSessionsNeedingBackup(limit) {
return new Promise((resolve, reject) => {
const sessions = [];
const txn = this._db.transaction(
["sessions_needing_backup", "inbound_group_sessions"],
"readonly",
);
txn.onerror = reject;
txn.oncomplete = function() {
resolve(sessions);
};
const objectStore = txn.objectStore("sessions_needing_backup");
const sessionStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
const sessionGetReq = sessionStore.get(cursor.key);
sessionGetReq.onsuccess = function() {
sessions.push({
senderKey: sessionGetReq.result.senderCurve25519Key,
sessionId: sessionGetReq.result.sessionId,
sessionData: sessionGetReq.result.session,
});
};
if (!limit || sessions.length < limit) {
cursor.continue();
}
}
};
});
}
countSessionsNeedingBackup(txn) {
if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readonly");
}
const objectStore = txn.objectStore("sessions_needing_backup");
return new Promise((resolve, reject) => {
const req = objectStore.count();
req.onerror = reject;
req.onsuccess = () => resolve(req.result);
});
}
unmarkSessionsNeedingBackup(sessions, txn) {
if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readwrite");
}
const objectStore = txn.objectStore("sessions_needing_backup");
return Promise.all(sessions.map((session) => {
return new Promise((resolve, reject) => {
const req = objectStore.delete([session.senderKey, session.sessionId]);
req.onsuccess = resolve;
req.onerror = reject;
});
}));
}
markSessionsNeedingBackup(sessions, txn) {
if (!txn) {
txn = this._db.transaction("sessions_needing_backup", "readwrite");
}
const objectStore = txn.objectStore("sessions_needing_backup");
return Promise.all(sessions.map((session) => {
return new Promise((resolve, reject) => {
const req = objectStore.put({
senderCurve25519Key: session.senderKey,
sessionId: session.sessionId,
});
req.onsuccess = resolve;
req.onerror = reject;
});
}));
}
doTxn(mode, stores, func) {
const txn = this._db.transaction(stores, mode);
const promise = promiseifyTxn(txn);
@@ -471,7 +617,7 @@ export class Backend {
}
export function upgradeDatabase(db, oldVersion) {
console.log(
logger.log(
`Upgrading IndexedDBCryptoStore from version ${oldVersion}`
+ ` to ${VERSION}`,
);
@@ -498,6 +644,11 @@ export function upgradeDatabase(db, oldVersion) {
if (oldVersion < 6) {
db.createObjectStore("rooms");
}
if (oldVersion < 7) {
db.createObjectStore("sessions_needing_backup", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
// Expand as needed.
}
@@ -539,11 +690,12 @@ function promiseifyTxn(txn) {
}
resolve();
};
txn.onerror = () => {
txn.onerror = (event) => {
if (txn._mx_abortexception !== undefined) {
reject(txn._mx_abortexception);
} else {
reject(event.target.error);
}
reject();
};
txn.onabort = () => reject(txn._mx_abortexception);
});
+128 -19
View File
@@ -17,9 +17,11 @@ limitations under the License.
import Promise from 'bluebird';
import logger from '../../logger';
import LocalStorageCryptoStore from './localStorage-crypto-store';
import MemoryCryptoStore from './memory-crypto-store';
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';
import {InvalidCryptoStoreError} from '../../errors';
/**
* Internal module. indexeddb storage for e2e.
@@ -64,7 +66,7 @@ export default class IndexedDBCryptoStore {
return;
}
console.log(`connecting to indexeddb ${this._dbName}`);
logger.log(`connecting to indexeddb ${this._dbName}`);
const req = this._indexedDB.open(
this._dbName, IndexedDBCryptoStoreBackend.VERSION,
@@ -77,7 +79,7 @@ export default class IndexedDBCryptoStore {
};
req.onblocked = () => {
console.log(
logger.log(
`can't yet open IndexedDBCryptoStore because it is open elsewhere`,
);
};
@@ -89,20 +91,42 @@ export default class IndexedDBCryptoStore {
req.onsuccess = (r) => {
const db = r.target.result;
console.log(`connected to indexeddb ${this._dbName}`);
logger.log(`connected to indexeddb ${this._dbName}`);
resolve(new IndexedDBCryptoStoreBackend.Backend(db));
};
}).then((backend) => {
// Edge has IndexedDB but doesn't support compund keys which we use fairly extensively.
// Try a dummy query which will fail if the browser doesn't support compund keys, so
// we can fall back to a different backend.
return backend.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS],
(txn) => {
backend.getEndToEndInboundGroupSession('', '', txn, () => {});
}).then(() => {
return backend;
},
);
}).catch((e) => {
console.warn(
if (e.name === 'VersionError') {
logger.warn("Crypto DB is too new for us to use!", e);
// don't fall back to a different store: the user has crypto data
// in this db so we should use it or nothing at all.
throw new InvalidCryptoStoreError(InvalidCryptoStoreError.TOO_NEW);
}
logger.warn(
`unable to connect to indexeddb ${this._dbName}` +
`: falling back to localStorage store: ${e}`,
);
return new LocalStorageCryptoStore(global.localStorage);
}).catch((e) => {
console.warn(
`unable to open localStorage: falling back to in-memory store: ${e}`,
);
return new MemoryCryptoStore();
try {
return new LocalStorageCryptoStore(global.localStorage);
} catch (e) {
logger.warn(
`unable to open localStorage: falling back to in-memory store: ${e}`,
);
return new MemoryCryptoStore();
}
});
return this._backendPromise;
@@ -120,11 +144,11 @@ export default class IndexedDBCryptoStore {
return;
}
console.log(`Removing indexeddb instance: ${this._dbName}`);
logger.log(`Removing indexeddb instance: ${this._dbName}`);
const req = this._indexedDB.deleteDatabase(this._dbName);
req.onblocked = () => {
console.log(
logger.log(
`can't yet delete IndexedDBCryptoStore because it is open elsewhere`,
);
};
@@ -134,14 +158,14 @@ export default class IndexedDBCryptoStore {
};
req.onsuccess = () => {
console.log(`Removed indexeddb instance: ${this._dbName}`);
logger.log(`Removed indexeddb instance: ${this._dbName}`);
resolve();
};
}).catch((e) => {
// in firefox, with indexedDB disabled, this fails with a
// DOMError. We treat this as non-fatal, so that people can
// still use the app.
console.warn(`unable to delete IndexedDBCryptoStore: ${e}`);
logger.warn(`unable to delete IndexedDBCryptoStore: ${e}`);
});
}
@@ -193,6 +217,24 @@ export default class IndexedDBCryptoStore {
});
}
/**
* Look for room key requests by target device and state
*
* @param {string} userId Target user ID
* @param {string} deviceId Target device ID
* @param {Array<Number>} wantedStates list of acceptable states
*
* @return {Promise} resolves to a list of all the
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
*/
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
return this._connect().then((backend) => {
return backend.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, wantedStates,
);
});
}
/**
* Look for an existing room key request by id and state, and update it if
* found
@@ -270,7 +312,10 @@ export default class IndexedDBCryptoStore {
* @param {string} sessionId The ID of the session to retrieve
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called with A map from sessionId
* to Base64 end-to-end session.
* to session information object with 'session' key being the
* Base64 end-to-end session and lastReceivedMessageTs being the
* timestamp in milliseconds at which the session last received
* a message.
*/
getEndToEndSession(deviceKey, sessionId, txn, func) {
this._backendPromise.value().getEndToEndSession(deviceKey, sessionId, txn, func);
@@ -282,22 +327,36 @@ export default class IndexedDBCryptoStore {
* @param {string} deviceKey The public key of the other device.
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called with A map from sessionId
* to Base64 end-to-end session.
* to session information object with 'session' key being the
* Base64 end-to-end session and lastReceivedMessageTs being the
* timestamp in milliseconds at which the session last received
* a message.
*/
getEndToEndSessions(deviceKey, txn, func) {
this._backendPromise.value().getEndToEndSessions(deviceKey, txn, func);
}
/**
* Retrieve all end-to-end sessions
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called one for each session with
* an object with, deviceKey, lastReceivedMessageTs, sessionId
* and session keys.
*/
getAllEndToEndSessions(txn, func) {
this._backendPromise.value().getAllEndToEndSessions(txn, func);
}
/**
* 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.
* @param {string} sessionInfo Session information object
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndSession(deviceKey, sessionId, session, txn) {
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
this._backendPromise.value().storeEndToEndSession(
deviceKey, sessionId, session, txn,
deviceKey, sessionId, sessionInfo, txn,
);
}
@@ -407,6 +466,55 @@ export default class IndexedDBCryptoStore {
this._backendPromise.value().getEndToEndRooms(txn, func);
}
// session backups
/**
* Get the inbound group sessions that need to be backed up.
* @param {integer} limit The maximum number of sessions to retrieve. 0
* for no limit.
* @returns {Promise} resolves to an array of inbound group sessions
*/
getSessionsNeedingBackup(limit) {
return this._connect().then((backend) => {
return backend.getSessionsNeedingBackup(limit);
});
}
/**
* Count the inbound group sessions that need to be backed up.
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves to the number of sessions
*/
countSessionsNeedingBackup(txn) {
return this._connect().then((backend) => {
return backend.countSessionsNeedingBackup(txn);
});
}
/**
* Unmark sessions as needing to be backed up.
* @param {Array<object>} sessions The sessions that need to be backed up.
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves when the sessions are unmarked
*/
unmarkSessionsNeedingBackup(sessions, txn) {
return this._connect().then((backend) => {
return backend.unmarkSessionsNeedingBackup(sessions, txn);
});
}
/**
* Mark sessions as needing to be backed up.
* @param {Array<object>} sessions The sessions that need to be backed up.
* @param {*} txn An active transaction. See doTxn(). (optional)
* @returns {Promise} resolves when the sessions are marked
*/
markSessionsNeedingBackup(sessions, txn) {
return this._connect().then((backend) => {
return backend.markSessionsNeedingBackup(sessions, txn);
});
}
/**
* Perform a transaction on the crypto store. Any store methods
* that require a transaction (txn) object to be passed in may
@@ -440,3 +548,4 @@ IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';
+91 -5
View File
@@ -15,6 +15,8 @@ limitations under the License.
*/
import Promise from 'bluebird';
import logger from '../../logger';
import MemoryCryptoStore from './memory-crypto-store.js';
/**
@@ -32,6 +34,7 @@ const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup";
function keyEndToEndSessions(deviceKey) {
return E2E_PREFIX + "sessions/" + deviceKey;
@@ -65,7 +68,21 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
}
_getEndToEndSessions(deviceKey, txn, func) {
return getJsonItem(this.store, keyEndToEndSessions(deviceKey));
const sessions = getJsonItem(this.store, keyEndToEndSessions(deviceKey));
const fixedSessions = {};
// fix up any old sessions to be objects rather than just the base64 pickle
for (const [sid, val] of Object.entries(sessions || {})) {
if (typeof val === 'string') {
fixedSessions[sid] = {
session: val,
};
} else {
fixedSessions[sid] = val;
}
}
return fixedSessions;
}
getEndToEndSession(deviceKey, sessionId, txn, func) {
@@ -77,9 +94,20 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
func(this._getEndToEndSessions(deviceKey) || {});
}
storeEndToEndSession(deviceKey, sessionId, session, txn) {
getAllEndToEndSessions(txn, func) {
for (let i = 0; i < this.store.length; ++i) {
if (this.store.key(i).startsWith(keyEndToEndSessions(''))) {
const deviceKey = this.store.key(i).split('/')[1];
for (const sess of Object.values(this._getEndToEndSessions(deviceKey))) {
func(sess);
}
}
}
}
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
const sessions = this._getEndToEndSessions(deviceKey) || {};
sessions[sessionId] = session;
sessions[sessionId] = sessionInfo;
setJsonItem(
this.store, keyEndToEndSessions(deviceKey), sessions,
);
@@ -165,6 +193,64 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
func(result);
}
getSessionsNeedingBackup(limit) {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
const sessions = [];
for (const session in sessionsNeedingBackup) {
if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) {
// see getAllEndToEndInboundGroupSessions for the magic number explanations
const senderKey = session.substr(0, 43);
const sessionId = session.substr(44);
this.getEndToEndInboundGroupSession(
senderKey, sessionId, null,
(sessionData) => {
sessions.push({
senderKey: senderKey,
sessionId: sessionId,
sessionData: sessionData,
});
},
);
if (limit && session.length >= limit) {
break;
}
}
}
return Promise.resolve(sessions);
}
countSessionsNeedingBackup() {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
return Promise.resolve(Object.keys(sessionsNeedingBackup).length);
}
unmarkSessionsNeedingBackup(sessions) {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
for (const session of sessions) {
delete sessionsNeedingBackup[session.senderKey + '/' + session.sessionId];
}
setJsonItem(
this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup,
);
return Promise.resolve();
}
markSessionsNeedingBackup(sessions) {
const sessionsNeedingBackup
= getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {};
for (const session of sessions) {
sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true;
}
setJsonItem(
this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup,
);
return Promise.resolve();
}
/**
* Delete all data from this store.
*
@@ -199,8 +285,8 @@ function getJsonItem(store, key) {
// JSON.parse(null) === null, so this returns null.
return JSON.parse(store.getItem(key));
} catch (e) {
console.log("Error: Failed to get key %s: %s", key, e.stack || e);
console.log(e.stack);
logger.log("Error: Failed to get key %s: %s", key, e.stack || e);
logger.log(e.stack);
}
return null;
}
+69 -6
View File
@@ -17,6 +17,7 @@ limitations under the License.
import Promise from 'bluebird';
import logger from '../../logger';
import utils from '../../utils';
/**
@@ -41,6 +42,8 @@ export default class MemoryCryptoStore {
this._deviceData = null;
// roomId -> Opaque roomInfo object
this._rooms = {};
// Set of {senderCurve25519Key+'/'+sessionId}
this._sessionsNeedingBackup = {};
}
/**
@@ -71,7 +74,7 @@ export default class MemoryCryptoStore {
if (existing) {
// this entry matches the request - return it.
console.log(
logger.log(
`already have key request outstanding for ` +
`${requestBody.room_id} / ${requestBody.session_id}: ` +
`not sending another`,
@@ -81,7 +84,7 @@ export default class MemoryCryptoStore {
// we got to the end of the list without finding a match
// - add the new request.
console.log(
logger.log(
`enqueueing key request for ${requestBody.room_id} / ` +
requestBody.session_id,
);
@@ -144,6 +147,19 @@ export default class MemoryCryptoStore {
return Promise.resolve(null);
}
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
const results = [];
for (const req of this._outgoingRoomKeyRequests) {
for (const state of wantedStates) {
if (req.state === state && req.recipients.includes({userId, deviceId})) {
results.push(req);
}
}
}
return Promise.resolve(results);
}
/**
* Look for an existing room key request by id and state, and update it if
* found
@@ -163,7 +179,7 @@ export default class MemoryCryptoStore {
}
if (req.state != expectedState) {
console.warn(
logger.warn(
`Cannot update room key request from ${expectedState} ` +
`as it was already updated to ${req.state}`,
);
@@ -194,7 +210,7 @@ export default class MemoryCryptoStore {
}
if (req.state != expectedState) {
console.warn(
logger.warn(
`Cannot delete room key request in state ${req.state} `
+ `(expected ${expectedState})`,
);
@@ -233,13 +249,21 @@ export default class MemoryCryptoStore {
func(this._sessions[deviceKey] || {});
}
storeEndToEndSession(deviceKey, sessionId, session, txn) {
getAllEndToEndSessions(txn, func) {
for (const deviceSessions of Object.values(this._sessions)) {
for (const sess of Object.values(deviceSessions)) {
func(sess);
}
}
}
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
let deviceSessions = this._sessions[deviceKey];
if (deviceSessions === undefined) {
deviceSessions = {};
this._sessions[deviceKey] = deviceSessions;
}
deviceSessions[sessionId] = session;
deviceSessions[sessionId] = sessionInfo;
}
// Inbound Group Sessions
@@ -295,6 +319,45 @@ export default class MemoryCryptoStore {
func(this._rooms);
}
getSessionsNeedingBackup(limit) {
const sessions = [];
for (const session in this._sessionsNeedingBackup) {
if (this._inboundGroupSessions[session]) {
sessions.push({
senderKey: session.substr(0, 43),
sessionId: session.substr(44),
sessionData: this._inboundGroupSessions[session],
});
if (limit && session.length >= limit) {
break;
}
}
}
return Promise.resolve(sessions);
}
countSessionsNeedingBackup() {
return Promise.resolve(Object.keys(this._sessionsNeedingBackup).length);
}
unmarkSessionsNeedingBackup(sessions) {
for (const session of sessions) {
const sessionKey = session.senderKey + '/' + session.sessionId;
delete this._sessionsNeedingBackup[sessionKey];
}
return Promise.resolve();
}
markSessionsNeedingBackup(sessions) {
for (const session of sessions) {
const sessionKey = session.senderKey + '/' + session.sessionId;
this._sessionsNeedingBackup[sessionKey] = true;
}
return Promise.resolve();
}
// Session key backups
doTxn(mode, stores, func) {
return Promise.resolve(func(null));
}
+200
View File
@@ -0,0 +1,200 @@
/*
Copyright 2018 New Vector 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.
*/
/**
* Base class for verification methods.
* @module crypto/verification/Base
*/
import {MatrixEvent} from '../../models/event';
import {EventEmitter} from 'events';
export default class VerificationBase extends EventEmitter {
/**
* Base class for verification methods.
*
* <p>Once a verifier object is created, the verification can be started by
* calling the verify() method, which will return a promise that will
* resolve when the verification is completed, or reject if it could not
* complete.</p>
*
* <p>Subclasses must have a NAME class property.</p>
*
* @class
*
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
*
* @param {string} userId the user ID that is being verified
*
* @param {string} deviceId the device ID that is being verified
*
* @param {string} transactionId the transaction ID to be used when sending events
*
* @param {object} startEvent the m.key.verification.start event that
* initiated this verification, if any
*
* @param {object} request the key verification request object related to
* this verification, if any
*
* @param {object} parent parent verification for this verification, if any
*/
constructor(baseApis, userId, deviceId, transactionId, startEvent, request, parent) {
super();
this._baseApis = baseApis;
this.userId = userId;
this.deviceId = deviceId;
this.transactionId = transactionId;
this.startEvent = startEvent;
this.request = request;
this._parent = parent;
this._done = false;
this._promise = null;
}
_sendToDevice(type, content) {
if (this._done) {
return Promise.reject(new Error("Verification is already done"));
}
content.transaction_id = this.transactionId;
return this._baseApis.sendToDevice(type, {
[this.userId]: { [this.deviceId]: content },
});
}
_waitForEvent(type) {
if (this._done) {
return Promise.reject(new Error("Verification is already done"));
}
this._expectedEvent = type;
return new Promise((resolve, reject) => {
this._resolveEvent = resolve;
this._rejectEvent = reject;
});
}
handleEvent(e) {
if (this._done) {
return;
} else if (e.getType() === this._expectedEvent) {
this._expectedEvent = undefined;
this._rejectEvent = undefined;
this._resolveEvent(e);
} else {
this._expectedEvent = undefined;
const exception = new Error(
"Unexpected message: expecting " + this._expectedEvent
+ " but got " + e.getType(),
);
if (this._rejectEvent) {
const reject = this._rejectEvent;
this._rejectEvent = undefined;
reject(exception);
}
this.cancel(exception);
}
}
done() {
if (!this._done) {
this._resolve();
}
}
cancel(e) {
if (!this._done) {
if (this.userId && this.deviceId && this.transactionId) {
// send a cancellation to the other user (if it wasn't
// cancelled by the other user)
if (e instanceof MatrixEvent) {
const sender = e.getSender();
if (sender !== this.userId) {
const content = e.getContent();
if (e.getType() === "m.key.verification.cancel") {
content.code = content.code || "m.unknown";
content.reason = content.reason || content.body
|| "Unknown reason";
content.transaction_id = this.transactionId;
this._sendToDevice("m.key.verification.cancel", content);
} else {
this._sendToDevice("m.key.verification.cancel", {
code: "m.unknown",
reason: content.body || "Unknown reason",
transaction_id: this.transactionId,
});
}
}
} else {
this._sendToDevice("m.key.verification.cancel", {
code: "m.unknown",
reason: e.toString(),
transaction_id: this.transactionId,
});
}
}
if (this._promise !== null) {
this._reject(e);
} else {
this._promise = Promise.reject(e);
}
// Also emit a 'cancel' event that the app can listen for to detect cancellation
// before calling verify()
this.emit('cancel', e);
}
}
/**
* Begin the key verification
*
* @returns {Promise} Promise which resolves when the verification has
* completed.
*/
verify() {
if (this._promise) return this._promise;
this._promise = new Promise((resolve, reject) => {
this._resolve = (...args) => {
this._done = true;
resolve(...args);
};
this._reject = (...args) => {
this._done = true;
reject(...args);
};
});
if (this._doVerification && !this._started) {
this._started = true;
Promise.resolve(this._doVerification())
.then(this.done.bind(this), this.cancel.bind(this));
}
return this._promise;
}
async _verifyKeys(userId, keys, verifier) {
for (const [keyId, keyInfo] of Object.entries(keys)) {
const deviceId = keyId.split(':', 2)[1];
const device = await this._baseApis.getStoredDevice(userId, deviceId);
if (!device) {
throw new Error(`Could not find device ${deviceId}`);
} else {
await verifier(keyId, device, keyInfo);
}
}
for (const keyId of Object.keys(keys)) {
const deviceId = keyId.split(':', 2)[1];
await this._baseApis.setDeviceVerified(userId, deviceId);
}
}
}
+87
View File
@@ -0,0 +1,87 @@
/*
Copyright 2018 New Vector 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.
*/
/**
* Error messages.
*
* @module crypto/verification/Error
*/
import {MatrixEvent} from "../../models/event";
export function newVerificationError(code, reason, extradata) {
extradata = extradata || {};
extradata.code = code;
extradata.reason = reason;
return new MatrixEvent({
type: "m.key.verification.cancel",
content: extradata,
});
}
export function errorFactory(code, reason) {
return function(extradata) {
return newVerificationError(code, reason, extradata);
};
}
/**
* The verification was cancelled by the user.
*/
export const newUserCancelledError = errorFactory("m.user", "Cancelled by user");
/**
* The verification timed out.
*/
export const newTimeoutError = errorFactory("m.timeout", "Timed out");
/**
* The transaction is unknown.
*/
export const newUnknownTransactionError = errorFactory(
"m.unknown_transaction", "Unknown transaction",
);
/**
* An unknown method was selected.
*/
export const newUnknownMethodError = errorFactory("m.unknown_method", "Unknown method");
/**
* An unexpected message was sent.
*/
export const newUnexpectedMessageError = errorFactory(
"m.unexpected_message", "Unexpected message",
);
/**
* The key does not match.
*/
export const newKeyMismatchError = errorFactory(
"m.key_mismatch", "Key mismatch",
);
/**
* The user does not match.
*/
export const newUserMismatchError = errorFactory("m.user_error", "User mismatch");
/**
* An invalid message was sent.
*/
export const newInvalidMessageError = errorFactory(
"m.invalid_message", "Invalid message",
);
+123
View File
@@ -0,0 +1,123 @@
/*
Copyright 2018 New Vector 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.
*/
/**
* QR code key verification.
* @module crypto/verification/QRCode
*/
import Base from "./Base";
import {
errorFactory,
newUserCancelledError,
newKeyMismatchError,
newUserMismatchError,
} from './Error';
const MATRIXTO_REGEXP = /^(?:https?:\/\/)?(?:www\.)?matrix\.to\/#\/([#@!+][^?]+)\?(.+)$/;
const KEY_REGEXP = /^key_([^:]+:.+)$/;
const newQRCodeError = errorFactory("m.qr_code.invalid", "Invalid QR code");
/**
* @class crypto/verification/QRCode/ShowQRCode
* @extends {module:crypto/verification/Base}
*/
export class ShowQRCode extends Base {
_doVerification() {
if (!this._done) {
const url = "https://matrix.to/#/" + this._baseApis.getUserId()
+ "?device=" + encodeURIComponent(this._baseApis.deviceId)
+ "&action=verify&key_ed25519%3A"
+ encodeURIComponent(this._baseApis.deviceId) + "="
+ encodeURIComponent(this._baseApis.getDeviceEd25519Key());
this.emit("show_qr_code", {
url: url,
});
}
}
}
ShowQRCode.NAME = "m.qr_code.show.v1";
/**
* @class crypto/verification/QRCode/ScanQRCode
* @extends {module:crypto/verification/Base}
*/
export class ScanQRCode extends Base {
static factory(...args) {
return new ScanQRCode(...args);
}
async _doVerification() {
const code = await new Promise((resolve, reject) => {
this.emit("scan", {
done: resolve,
cancel: () => reject(newUserCancelledError()),
});
});
const match = code.match(MATRIXTO_REGEXP);
let deviceId;
const keys = {};
if (!match) {
throw newQRCodeError();
}
const userId = match[1];
const params = match[2].split("&").map(
(x) => x.split("=", 2).map(decodeURIComponent),
);
let action;
for (const [name, value] of params) {
if (name === "device") {
deviceId = value;
} else if (name === "action") {
action = value;
} else {
const keyMatch = name.match(KEY_REGEXP);
if (keyMatch) {
keys[keyMatch[1]] = value;
}
}
}
if (!deviceId || action !== "verify" || Object.keys(keys).length === 0) {
throw newQRCodeError();
}
if (!this.userId) {
await new Promise((resolve, reject) => {
this.emit("confirm_user_id", {
userId: userId,
confirm: resolve,
cancel: () => reject(newUserMismatchError()),
});
});
} else if (this.userId !== userId) {
throw newUserMismatchError({
expected: this.userId,
actual: userId,
});
}
await this._verifyKeys(userId, keys, (keyId, device, key) => {
if (device.keys[keyId] !== key) {
throw newKeyMismatchError();
}
});
}
}
ScanQRCode.NAME = "m.qr_code.scan.v1";
+368
View File
@@ -0,0 +1,368 @@
/*
Copyright 2018 New Vector 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.
*/
/**
* Short Authentication String (SAS) verification.
* @module crypto/verification/SAS
*/
import Base from "./Base";
import anotherjson from 'another-json';
import {
errorFactory,
newUserCancelledError,
newUnknownMethodError,
newKeyMismatchError,
newInvalidMessageError,
} from './Error';
const EVENTS = [
"m.key.verification.accept",
"m.key.verification.key",
"m.key.verification.mac",
];
let olmutil;
const newMismatchedSASError = errorFactory(
"m.mismatched_sas", "Mismatched short authentication string",
);
const newMismatchedCommitmentError = errorFactory(
"m.mismatched_commitment", "Mismatched commitment",
);
function generateDecimalSas(sasBytes) {
/**
* +--------+--------+--------+--------+--------+
* | Byte 0 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
* +--------+--------+--------+--------+--------+
* bits: 87654321 87654321 87654321 87654321 87654321
* \____________/\_____________/\____________/
* 1st number 2nd number 3rd number
*/
return [
(sasBytes[0] << 5 | sasBytes[1] >> 3) + 1000,
((sasBytes[1] & 0x7) << 10 | sasBytes[2] << 2 | sasBytes[3] >> 6) + 1000,
((sasBytes[3] & 0x3f) << 7 | sasBytes[4] >> 1) + 1000,
];
}
const emojiMapping = [
["🐶", "dog"], // 0
["🐱", "cat"], // 1
["🦁", "lion"], // 2
["🐎", "horse"], // 3
["🦄", "unicorn"], // 4
["🐷", "pig"], // 5
["🐘", "elephant"], // 6
["🐰", "rabbit"], // 7
["🐼", "panda"], // 8
["🐓", "rooster"], // 9
["🐧", "penguin"], // 10
["🐢", "turtle"], // 11
["🐟", "fish"], // 12
["🐙", "octopus"], // 13
["🦋", "butterfly"], // 14
["🌷", "flower"], // 15
["🌳", "tree"], // 16
["🌵", "cactus"], // 17
["🍄", "mushroom"], // 18
["🌏", "globe"], // 19
["🌙", "moon"], // 20
["☁️", "cloud"], // 21
["🔥", "fire"], // 22
["🍌", "banana"], // 23
["🍎", "apple"], // 24
["🍓", "strawberry"], // 25
["🌽", "corn"], // 26
["🍕", "pizza"], // 27
["🎂", "cake"], // 28
["❤️", "heart"], // 29
["🙂", "smiley"], // 30
["🤖", "robot"], // 31
["🎩", "hat"], // 32
["👓", "glasses"], // 33
["🔧", "spanner"], // 34
["🎅", "santa"], // 35
["👍", "thumbs up"], // 36
["☂️", "umbrella"], // 37
["⌛", "hourglass"], // 38
["⏰", "clock"], // 39
["🎁", "gift"], // 40
["💡", "light bulb"], // 41
["📕", "book"], // 42
["✏️", "pencil"], // 43
["📎", "paperclip"], // 44
["✂️", "scissors"], // 45
["🔒", "padlock"], // 46
["🔑", "key"], // 47
["🔨", "hammer"], // 48
["☎️", "telephone"], // 49
["🏁", "flag"], // 50
["🚂", "train"], // 51
["🚲", "bicycle"], // 52
["✈️", "aeroplane"], // 53
["🚀", "rocket"], // 54
["🏆", "trophy"], // 55
["⚽", "ball"], // 56
["🎸", "guitar"], // 57
["🎺", "trumpet"], // 58
["🔔", "bell"], // 59
["⚓️", "anchor"], // 60
["🎧", "headphones"], // 61
["📁", "folder"], // 62
["📌", "pin"], // 63
];
function generateEmojiSas(sasBytes) {
const emojis = [
// just like base64 encoding
sasBytes[0] >> 2,
(sasBytes[0] & 0x3) << 4 | sasBytes[1] >> 4,
(sasBytes[1] & 0xf) << 2 | sasBytes[2] >> 6,
sasBytes[2] & 0x3f,
sasBytes[3] >> 2,
(sasBytes[3] & 0x3) << 4 | sasBytes[4] >> 4,
(sasBytes[4] & 0xf) << 2 | sasBytes[5] >> 6,
];
return emojis.map((num) => emojiMapping[num]);
}
function generateSas(sasBytes, methods) {
const sas = {};
if (methods.includes("decimal")) {
sas["decimal"] = generateDecimalSas(sasBytes);
}
if (methods.includes("emoji")) {
sas["emoji"] = generateEmojiSas(sasBytes);
}
return sas;
}
/**
* @alias module:crypto/verification/SAS
* @extends {module:crypto/verification/Base}
*/
export default class SAS extends Base {
get events() {
return EVENTS;
}
async _doVerification() {
await global.Olm.init();
olmutil = olmutil || new global.Olm.Utility();
// make sure user's keys are downloaded
await this._baseApis.downloadKeys([this.userId]);
if (this.startEvent) {
return await this._doRespondVerification();
} else {
return await this._doSendVerification();
}
}
async _doSendVerification() {
const initialMessage = {
method: SAS.NAME,
from_device: this._baseApis.deviceId,
key_agreement_protocols: ["curve25519"],
hashes: ["sha256"],
message_authentication_codes: ["hmac-sha256"],
// FIXME: allow app to specify what SAS methods can be used
short_authentication_string: ["decimal", "emoji"],
transaction_id: this.transactionId,
};
this._sendToDevice("m.key.verification.start", initialMessage);
let e = await this._waitForEvent("m.key.verification.accept");
let content = e.getContent();
if (!(content.key_agreement_protocol === "curve25519"
&& content.hash === "sha256"
&& content.message_authentication_code === "hmac-sha256"
&& content.short_authentication_string instanceof Array
&& (content.short_authentication_string.includes("decimal")
|| content.short_authentication_string.includes("emoji")))) {
throw newUnknownMethodError();
}
if (typeof content.commitment !== "string") {
throw newInvalidMessageError();
}
const hashCommitment = content.commitment;
const sasMethods = content.short_authentication_string;
const olmSAS = new global.Olm.SAS();
try {
this._sendToDevice("m.key.verification.key", {
key: olmSAS.get_pubkey(),
});
e = await this._waitForEvent("m.key.verification.key");
// FIXME: make sure event is properly formed
content = e.getContent();
const commitmentStr = content.key + anotherjson.stringify(initialMessage);
if (olmutil.sha256(commitmentStr) !== hashCommitment) {
throw newMismatchedCommitmentError();
}
olmSAS.set_their_key(content.key);
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.userId + this.deviceId
+ this.transactionId;
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
const verifySAS = new Promise((resolve, reject) => {
this.emit("show_sas", {
sas: generateSas(sasBytes, sasMethods),
confirm: () => {
this._sendMAC(olmSAS);
resolve();
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
});
});
[e] = await Promise.all([
this._waitForEvent("m.key.verification.mac"),
verifySAS,
]);
content = e.getContent();
await this._checkMAC(olmSAS, content);
} finally {
olmSAS.free();
}
}
async _doRespondVerification() {
let content = this.startEvent.getContent();
if (!(content.key_agreement_protocols instanceof Array
&& content.key_agreement_protocols.includes("curve25519")
&& content.hashes instanceof Array
&& content.hashes.includes("sha256")
&& content.message_authentication_codes instanceof Array
&& content.message_authentication_codes.includes("hmac-sha256")
&& content.short_authentication_string instanceof Array
&& (content.short_authentication_string.includes("decimal")
|| content.short_authentication_string.includes("emoji")))) {
throw newUnknownMethodError();
}
const olmSAS = new global.Olm.SAS();
const sasMethods = [];
// FIXME: allow app to specify what SAS methods can be used
if (content.short_authentication_string.includes("decimal")) {
sasMethods.push("decimal");
}
if (content.short_authentication_string.includes("emoji")) {
sasMethods.push("emoji");
}
try {
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
this._sendToDevice("m.key.verification.accept", {
key_agreement_protocol: "curve25519",
hash: "sha256",
message_authentication_code: "hmac-sha256",
short_authentication_string: sasMethods,
commitment: olmutil.sha256(commitmentStr),
});
let e = await this._waitForEvent("m.key.verification.key");
// FIXME: make sure event is properly formed
content = e.getContent();
olmSAS.set_their_key(content.key);
this._sendToDevice("m.key.verification.key", {
key: olmSAS.get_pubkey(),
});
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
+ this.userId + this.deviceId
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.transactionId;
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
const verifySAS = new Promise((resolve, reject) => {
this.emit("show_sas", {
sas: generateSas(sasBytes, sasMethods),
confirm: () => {
this._sendMAC(olmSAS);
resolve();
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
});
});
[e] = await Promise.all([
this._waitForEvent("m.key.verification.mac"),
verifySAS,
]);
content = e.getContent();
await this._checkMAC(olmSAS, content);
} finally {
olmSAS.free();
}
}
_sendMAC(olmSAS) {
const keyId = `ed25519:${this._baseApis.deviceId}`;
const mac = {};
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.userId + this.deviceId
+ this.transactionId;
mac[keyId] = olmSAS.calculate_mac(
this._baseApis.getDeviceEd25519Key(),
baseInfo + keyId,
);
const keys = olmSAS.calculate_mac(
keyId,
baseInfo + "KEY_IDS",
);
this._sendToDevice("m.key.verification.mac", { mac, keys });
}
async _checkMAC(olmSAS, content) {
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this.userId + this.deviceId
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.transactionId;
if (content.keys !== olmSAS.calculate_mac(
Object.keys(content.mac).sort().join(","),
baseInfo + "KEY_IDS",
)) {
throw newKeyMismatchError();
}
await this._verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
if (keyInfo !== olmSAS.calculate_mac(
device.keys[keyId],
baseInfo + keyId,
)) {
throw newKeyMismatchError();
}
});
}
}
SAS.NAME = "m.sas.v1";
+46
View File
@@ -0,0 +1,46 @@
// can't just do InvalidStoreError extends Error
// because of http://babeljs.io/docs/usage/caveats/#classes
export function InvalidStoreError(reason, value) {
const message = `Store is invalid because ${reason}, ` +
`please stop the client, delete all data and start the client again`;
const instance = Reflect.construct(Error, [message]);
Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this));
instance.reason = reason;
instance.value = value;
return instance;
}
InvalidStoreError.TOGGLED_LAZY_LOADING = "TOGGLED_LAZY_LOADING";
InvalidStoreError.prototype = Object.create(Error.prototype, {
constructor: {
value: Error,
enumerable: false,
writable: true,
configurable: true,
},
});
Reflect.setPrototypeOf(InvalidStoreError, Error);
export function InvalidCryptoStoreError(reason) {
const message = `Crypto store is invalid because ${reason}, ` +
`please stop the client, delete all data and start the client again`;
const instance = Reflect.construct(Error, [message]);
Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this));
instance.reason = reason;
instance.name = 'InvalidCryptoStoreError';
return instance;
}
InvalidCryptoStoreError.TOO_NEW = "TOO_NEW";
InvalidCryptoStoreError.prototype = Object.create(Error.prototype, {
constructor: {
value: Error,
enumerable: false,
writable: true,
configurable: true,
},
});
Reflect.setPrototypeOf(InvalidCryptoStoreError, Error);
+11
View File
@@ -51,6 +51,17 @@ function Filter(userId, filterId) {
this.definition = {};
}
Filter.LAZY_LOADING_MESSAGES_FILTER = {
lazy_load_members: true,
};
Filter.LAZY_LOADING_SYNC_FILTER = {
room: {
state: Filter.LAZY_LOADING_MESSAGES_FILTER,
},
};
/**
* Get the ID of this filter on your homeserver (if known)
* @return {?Number} The filter ID
+39 -6
View File
@@ -118,6 +118,10 @@ module.exports.MatrixHttpApi.prototype = {
* @param {string=} opts.name Name to give the file on the server. Defaults
* to <tt>file.name</tt>.
*
* @param {boolean=} opts.includeFilename if false will not send the filename,
* e.g for encrypted file uploads where filename leaks are undesirable.
* Defaults to true.
*
* @param {string=} opts.type Content-type for the upload. Defaults to
* <tt>file.type</tt>, or <tt>applicaton/octet-stream</tt>.
*
@@ -152,6 +156,9 @@ module.exports.MatrixHttpApi.prototype = {
opts = {};
}
// default opts.includeFilename to true (ignoring falsey values)
const includeFilename = opts.includeFilename !== false;
// 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';
@@ -272,10 +279,26 @@ module.exports.MatrixHttpApi.prototype = {
}
});
let url = this.opts.baseUrl + "/_matrix/media/v1/upload";
url += "?access_token=" + encodeURIComponent(this.opts.accessToken);
url += "&filename=" + encodeURIComponent(fileName);
const queryArgs = [];
if (includeFilename && fileName) {
queryArgs.push("filename=" + encodeURIComponent(fileName));
}
if (!this.useAuthorizationHeader) {
queryArgs.push("access_token="
+ encodeURIComponent(this.opts.accessToken));
}
if (queryArgs.length > 0) {
url += "?" + queryArgs.join("&");
}
xhr.open("POST", url);
if (this.useAuthorizationHeader) {
xhr.setRequestHeader("Authorization", "Bearer " + this.opts.accessToken);
}
xhr.setRequestHeader("Content-Type", contentType);
xhr.send(body);
promise = defer.promise;
@@ -283,9 +306,11 @@ module.exports.MatrixHttpApi.prototype = {
// dirty hack (as per _request) to allow the upload to be cancelled.
promise.abort = xhr.abort.bind(xhr);
} else {
const queryParams = {
filename: fileName,
};
const queryParams = {};
if (includeFilename && fileName) {
queryParams.filename = fileName;
}
promise = this.authedRequest(
opts.callback, "POST", "/upload", queryParams, body, {
@@ -433,6 +458,12 @@ module.exports.MatrixHttpApi.prototype = {
requestPromise.catch(function(err) {
if (err.errcode == 'M_UNKNOWN_TOKEN') {
self.event_emitter.emit("Session.logged_out");
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
self.event_emitter.emit(
"no_consent",
err.message,
err.data.consent_uri,
);
}
});
@@ -721,10 +752,12 @@ module.exports.MatrixHttpApi.prototype = {
method: method,
withCredentials: false,
qs: queryParams,
qsStringifyOptions: opts.qsStringifyOptions,
useQuerystring: true,
body: data,
json: false,
timeout: localTimeoutMs,
headers: opts.headers || {},
headers: headers || {},
_matrix_opts: this.opts,
},
function(err, response, body) {
+36
View File
@@ -0,0 +1,36 @@
/*
Copyright 2018 André Jaenisch
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 logger
*/
const log = require("loglevel");
// This is to demonstrate, that you can use any namespace you want.
// Namespaces allow you to turn on/off the logging for specific parts of the
// application.
// An idea would be to control this via an environment variable (on Node.js).
// See https://www.npmjs.com/package/debug to see how this could be implemented
// Part of #332 is introducing a logging library in the first place.
const DEFAULT_NAME_SPACE = "matrix";
const logger = log.getLogger(DEFAULT_NAME_SPACE);
logger.setLevel(log.levels.DEBUG);
/**
* Drop-in replacement for <code>console</code> using {@link https://www.npmjs.com/package/loglevel|loglevel}.
* Can be tailored down to specific use cases if needed.
*/
module.exports = logger;
+15 -2
View File
@@ -34,10 +34,14 @@ module.exports.SyncAccumulator = require("./sync-accumulator");
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:errors.InvalidStoreError|InvalidStoreError} class. */
module.exports.InvalidStoreError = require("./errors").InvalidStoreError;
/** The {@link module:client.MatrixClient|MatrixClient} class. */
module.exports.MatrixClient = require("./client").MatrixClient;
/** The {@link module:models/room|Room} class. */
module.exports.Room = require("./models/room");
/** The {@link module:models/group|Group} class. */
module.exports.Group = require("./models/group");
/** The {@link module:models/event-timeline~EventTimeline} class. */
module.exports.EventTimeline = require("./models/event-timeline");
/** The {@link module:models/event-timeline-set~EventTimelineSet} class. */
@@ -63,6 +67,8 @@ module.exports.Filter = require("./filter");
module.exports.TimelineWindow = require("./timeline-window").TimelineWindow;
/** The {@link module:interactive-auth} class. */
module.exports.InteractiveAuth = require("./interactive-auth");
/** The {@link module:auto-discovery|AutoDiscovery} class. */
module.exports.AutoDiscovery = require("./autodiscovery").AutoDiscovery;
module.exports.MemoryCryptoStore =
@@ -82,14 +88,21 @@ module.exports.createNewMatrixCall = require("./webrtc/call").createNewMatrixCal
/**
* Set an audio input device to use for MatrixCalls
* Set a preferred audio output device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setMatrixCallAudioOutput = require('./webrtc/call').setAudioOutput;
/**
* Set a preferred audio input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setMatrixCallAudioInput = require('./webrtc/call').setAudioInput;
/**
* Set a video input device to use for MatrixCalls
* Set a preferred video input device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
+7 -37
View File
@@ -168,49 +168,19 @@ EventTimelineSet.prototype.resetLiveTimeline = function(
// if timeline support is disabled, forget about the old timelines
const resetAllTimelines = !this._timelineSupport || !forwardPaginationToken;
let newTimeline;
const oldTimeline = this._liveTimeline;
const newTimeline = resetAllTimelines ?
oldTimeline.forkLive(EventTimeline.FORWARDS) :
oldTimeline.fork(EventTimeline.FORWARDS);
if (resetAllTimelines) {
newTimeline = new EventTimeline(this);
this._timelines = [newTimeline];
this._eventIdToTimeline = {};
} else {
newTimeline = this.addTimeline();
this._timelines.push(newTimeline);
}
const oldTimeline = this._liveTimeline;
// Collect the state events from the old timeline
const evMap = oldTimeline.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]);
}
}
// Use those events to initialise the state of the new live timeline
newTimeline.initialiseState(events);
const freshEndState = newTimeline._endState;
// Now clobber the end state of the new live timeline with that from the
// previous live timeline. It will be identical except that we'll keep
// using the same RoomMember objects for the 'live' set of members with any
// listeners still attached
newTimeline._endState = oldTimeline._endState;
// If we're not resetting all timelines, we need to fix up the old live timeline
if (!resetAllTimelines) {
// Firstly, we just stole the old timeline's end state, so it needs a new one.
// Just swap them around and give it the one we just generated for the
// new live timeline.
oldTimeline._endState = freshEndState;
if (forwardPaginationToken) {
// Now set the forward pagination token on the old live timeline
// so it can be forward-paginated.
oldTimeline.setPaginationToken(
+44
View File
@@ -106,6 +106,50 @@ EventTimeline.prototype.initialiseState = function(stateEvents) {
this._endState.setStateEvents(stateEvents);
};
/**
* Forks the (live) timeline, taking ownership of the existing directional state of this timeline.
* All attached listeners will keep receiving state updates from the new live timeline state.
* The end state of this timeline gets replaced with an independent copy of the current RoomState,
* and will need a new pagination token if it ever needs to paginate forwards.
* @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 {EventTimeline} the new timeline
*/
EventTimeline.prototype.forkLive = function(direction) {
const forkState = this.getState(direction);
const timeline = new EventTimeline(this._eventTimelineSet);
timeline._startState = forkState.clone();
// Now clobber the end state of the new live timeline with that from the
// previous live timeline. It will be identical except that we'll keep
// using the same RoomMember objects for the 'live' set of members with any
// listeners still attached
timeline._endState = forkState;
// Firstly, we just stole the current timeline's end state, so it needs a new one.
// Make an immutable copy of the state so back pagination will get the correct sentinels.
this._endState = forkState.clone();
return timeline;
};
/**
* Creates an independent timeline, inheriting the directional state from this 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 {EventTimeline} the new timeline
*/
EventTimeline.prototype.fork = function(direction) {
const forkState = this.getState(direction);
const timeline = new EventTimeline(this._eventTimelineSet);
timeline._startState = forkState.clone();
timeline._endState = forkState.clone();
return timeline;
};
/**
* Get the ID of the room for this timeline
* @return {string} room ID
+57 -4
View File
@@ -382,15 +382,41 @@ utils.extend(module.exports.MatrixEvent.prototype, {
* Cancel any room key request for this event and resend another.
*
* @param {module:crypto} crypto crypto module
* @param {string} userId the user who received this event
*
* @returns {Promise} a promise that resolves when the request is queued
*/
cancelAndResendKeyRequest: function(crypto) {
cancelAndResendKeyRequest: function(crypto, userId) {
const wireContent = this.getWireContent();
crypto.cancelRoomKeyRequest({
return crypto.requestRoomKey({
algorithm: wireContent.algorithm,
room_id: this.getRoomId(),
session_id: wireContent.session_id,
sender_key: wireContent.sender_key,
}, true);
}, this.getKeyRequestRecipients(userId), true);
},
/**
* Calculate the recipients for keyshare requests.
*
* @param {string} userId the user who received this event.
*
* @returns {Array} array of recipients
*/
getKeyRequestRecipients: function(userId) {
// send the request to all of our own devices, and the
// original sending device if it wasn't us.
const wireContent = this.getWireContent();
const recipients = [{
userId, deviceId: '*',
}];
const sender = this.getSender();
if (sender !== userId) {
recipients.push({
userId: sender, deviceId: wireContent.device_id,
});
}
return recipients;
},
_decryptionLoop: async function(crypto) {
@@ -404,6 +430,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
this._retryDecryption = false;
let res;
let err;
try {
if (!crypto) {
res = this._badEncryptedMessage("Encryption not enabled");
@@ -422,6 +449,8 @@ utils.extend(module.exports.MatrixEvent.prototype, {
return;
}
err = e;
// see if we have a retry queued.
//
// NB: make sure to keep this check in the same tick of the
@@ -467,6 +496,17 @@ utils.extend(module.exports.MatrixEvent.prototype, {
this._decryptionPromise = null;
this._retryDecryption = false;
this._setClearData(res);
// Before we emit the event, clear the push actions so that they can be recalculated
// by relevant code. We do this because the clear event has now changed, making it
// so that existing rules can be re-run over the applicable properties. Stuff like
// highlighting when the user's name is mentioned rely on this happening. We also want
// to set the push actions before emitting so that any notification listeners don't
// pick up the wrong contents.
this.setPushActions(null);
this.emit("Event.decrypted", this, err);
return;
}
},
@@ -503,7 +543,17 @@ utils.extend(module.exports.MatrixEvent.prototype, {
decryptionResult.claimedEd25519Key || null;
this._forwardingCurve25519KeyChain =
decryptionResult.forwardingCurve25519KeyChain || [];
this.emit("Event.decrypted", this);
},
/**
* Gets the cleartext content for this event. If the event is not encrypted,
* or encryption has not been completed, this will return null.
*
* @returns {Object} The cleartext (decrypted) content for the event
*/
getClearContent: function() {
const ev = this._clearEvent;
return ev && ev.content ? ev.content : null;
},
/**
@@ -708,4 +758,7 @@ const _REDACT_KEEP_CONTENT_MAP = {
*
* @param {module:models/event.MatrixEvent} event
* The matrix event which has been decrypted
* @param {module:crypto/algorithms/base.DecryptionError?} err
* The error that occured during decryption, or `undefined` if no
* error occured.
*/
+90 -14
View File
@@ -58,10 +58,27 @@ function RoomMember(roomId, userId) {
this.events = {
member: null,
};
this._isOutOfBand = false;
this._updateModifiedTime();
}
utils.inherits(RoomMember, EventEmitter);
/**
* Mark the member as coming from a channel that is not sync
*/
RoomMember.prototype.markOutOfBand = function() {
this._isOutOfBand = true;
};
/**
* @return {bool} does the member come from a channel that is not sync?
* This is used to store the member seperately
* from the sync state so it available across browser sessions.
*/
RoomMember.prototype.isOutOfBand = function() {
return this._isOutOfBand;
};
/**
* Update this room member's membership event. May fire "RoomMember.name" if
* this event updates this member's name.
@@ -75,13 +92,20 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) {
if (event.getType() !== "m.room.member") {
return;
}
this._isOutOfBand = false;
this.events.member = event;
const oldMembership = this.membership;
this.membership = event.getDirectionalContent().membership;
const oldName = this.name;
this.name = calculateDisplayName(this, event, roomState);
this.name = calculateDisplayName(
this.userId,
event.getDirectionalContent().displayname,
roomState);
this.rawDisplayName = event.getDirectionalContent().displayname || this.userId;
if (oldMembership !== this.membership) {
this._updateModifiedTime();
@@ -177,6 +201,44 @@ RoomMember.prototype.getLastModifiedTime = function() {
return this._modified;
};
RoomMember.prototype.isKicked = function() {
return this.membership === "leave" &&
this.events.member.getSender() !== this.events.member.getStateKey();
};
/**
* If this member was invited with the is_direct flag set, return
* the user that invited this member
* @return {string} user id of the inviter
*/
RoomMember.prototype.getDMInviter = function() {
// when not available because that room state hasn't been loaded in,
// we don't really know, but more likely to not be a direct chat
if (this.events.member) {
// TODO: persist the is_direct flag on the member as more member events
// come in caused by displayName changes.
// the is_direct flag is set on the invite member event.
// This is copied on the prev_content section of the join member event
// when the invite is accepted.
const memberEvent = this.events.member;
let memberContent = memberEvent.getContent();
let inviteSender = memberEvent.getSender();
if (memberContent.membership === "join") {
memberContent = memberEvent.getPrevContent();
inviteSender = memberEvent.getUnsigned().prev_sender;
}
if (memberContent.membership === "invite" && memberContent.is_direct) {
return inviteSender;
}
}
};
/**
* Get the avatar URL for a room member.
* @param {string} baseUrl The base homeserver URL See
@@ -200,10 +262,12 @@ RoomMember.prototype.getAvatarUrl =
if (allowDefault === undefined) {
allowDefault = true;
}
if (!this.events.member && !allowDefault) {
const rawUrl = this.getMxcAvatarUrl();
if (!rawUrl && !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,
);
@@ -216,12 +280,27 @@ RoomMember.prototype.getAvatarUrl =
}
return null;
};
/**
* get the mxc avatar url, either from a state event, or from a lazily loaded member
* @return {string} the mxc avatar url
*/
RoomMember.prototype.getMxcAvatarUrl = function() {
if(this.events.member) {
return this.events.member.getDirectionalContent().avatar_url;
} else if(this.user) {
return this.user.avatarUrl;
}
return null;
};
function calculateDisplayName(member, event, roomState) {
const displayName = event.getDirectionalContent().displayname;
const selfUserId = member.userId;
function calculateDisplayName(selfUserId, displayName, roomState) {
if (!displayName || displayName === selfUserId) {
return selfUserId;
}
if (!displayName) {
// First check if the displayname is something we consider truthy
// after stripping it of zero width characters and padding spaces
if (!utils.removeHiddenChars(displayName)) {
return selfUserId;
}
@@ -229,18 +308,15 @@ function calculateDisplayName(member, event, roomState) {
return displayName;
}
// Check if the name contains something that look like a mxid
// Next check if the name contains something that look like a mxid
// If it does, it may be someone trying to impersonate someone else
// Show full mxid in this case
// Also show mxid if there are other people with the same displayname
// Also show mxid if there are other people with the same or similar
// displayname, after hidden character removal.
let disambiguate = /@.+:.+/.test(displayName);
if (!disambiguate) {
const userIds = roomState.getUserIdsWithDisplayName(displayName);
const otherUsers = userIds.filter(function(u) {
return u !== selfUserId;
});
disambiguate = otherUsers.length > 0;
disambiguate = userIds.some((u) => u !== selfUserId);
}
if (disambiguate) {
+310 -55
View File
@@ -22,6 +22,11 @@ const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
const RoomMember = require("./room-member");
// possible statuses for out-of-band member loading
const OOB_STATUS_NOTSTARTED = 1;
const OOB_STATUS_INPROGRESS = 2;
const OOB_STATUS_FINISHED = 3;
/**
* Construct room state.
*
@@ -46,13 +51,17 @@ const RoomMember = require("./room-member");
* @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
* @param {?object} oobMemberFlags Optional. The state of loading out of bound members.
* As the timeline might get reset while they are loading, this state needs to be inherited
* and shared when the room state is cloned for the new timeline.
* This should only be passed from clone.
* @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) {
function RoomState(roomId, oobMemberFlags = undefined) {
this.roomId = roomId;
this.members = {
// userId: RoomMember
@@ -66,10 +75,28 @@ function RoomState(roomId) {
// userId: RoomMember
};
this._updateModifiedTime();
// stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys)
this._displayNameToUserIds = {};
this._userIdsToDisplayNames = {};
this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite
this._joinedMemberCount = null; // cache of the number of joined members
// joined members count from summary api
// once set, we know the server supports the summary api
// and we should only trust that
// we could also only trust that before OOB members
// are loaded but doesn't seem worth the hassle atm
this._summaryJoinedMemberCount = null;
// same for invited member count
this._invitedMemberCount = null;
this._summaryInvitedMemberCount = null;
if (!oobMemberFlags) {
oobMemberFlags = {
status: OOB_STATUS_NOTSTARTED,
};
}
this._oobMemberFlags = oobMemberFlags;
}
utils.inherits(RoomState, EventEmitter);
@@ -79,14 +106,48 @@ utils.inherits(RoomState, EventEmitter);
* @return {integer} The number of members in this room whose membership is 'join'
*/
RoomState.prototype.getJoinedMemberCount = function() {
if (this._summaryJoinedMemberCount !== null) {
return this._summaryJoinedMemberCount;
}
if (this._joinedMemberCount === null) {
this._joinedMemberCount = this.getMembers().filter((m) => {
return m.membership === 'join';
}).length;
this._joinedMemberCount = this.getMembers().reduce((count, m) => {
return m.membership === 'join' ? count + 1 : count;
}, 0);
}
return this._joinedMemberCount;
};
/**
* Set the joined member count explicitly (like from summary part of the sync response)
* @param {number} count the amount of joined members
*/
RoomState.prototype.setJoinedMemberCount = function(count) {
this._summaryJoinedMemberCount = count;
};
/**
* Returns the number of invited members in this room
* @return {integer} The number of members in this room whose membership is 'invite'
*/
RoomState.prototype.getInvitedMemberCount = function() {
if (this._summaryInvitedMemberCount !== null) {
return this._summaryInvitedMemberCount;
}
if (this._invitedMemberCount === null) {
this._invitedMemberCount = this.getMembers().reduce((count, m) => {
return m.membership === 'invite' ? count + 1 : count;
}, 0);
}
return this._invitedMemberCount;
};
/**
* Set the amount of invited members in this room
* @param {number} count the amount of invited members
*/
RoomState.prototype.setInvitedMemberCount = function(count) {
this._summaryInvitedMemberCount = count;
};
/**
* Get all RoomMembers in this room.
* @return {Array<RoomMember>} A list of RoomMembers.
@@ -95,6 +156,16 @@ RoomState.prototype.getMembers = function() {
return utils.values(this.members);
};
/**
* Get all RoomMembers in this room, excluding the user IDs provided.
* @param {Array<string>} excludedIds The user IDs to exclude.
* @return {Array<RoomMember>} A list of RoomMembers.
*/
RoomState.prototype.getMembersExcept = function(excludedIds) {
return utils.values(this.members)
.filter((m) => !excludedIds.includes(m.userId));
};
/**
* Get a room member by their user ID.
* @param {string} userId The room member's user ID.
@@ -119,12 +190,9 @@ RoomState.prototype.getSentinelMember = function(userId) {
if (sentinel === undefined) {
sentinel = new RoomMember(this.roomId, userId);
const membershipEvent = this.getStateEvents("m.room.member", userId);
if (!membershipEvent) return null;
sentinel.setMembershipEvent(membershipEvent, this);
const pwrLvlEvent = this.getStateEvents("m.room.power_levels", "");
if (pwrLvlEvent) {
sentinel.setPowerLevelEvent(pwrLvlEvent);
const member = this.members[userId];
if (member) {
sentinel.setMembershipEvent(member.events.member, this);
}
this._sentinels[userId] = sentinel;
}
@@ -152,6 +220,67 @@ RoomState.prototype.getStateEvents = function(eventType, stateKey) {
return event ? event : null;
};
/**
* Creates a copy of this room state so that mutations to either won't affect the other.
* @return {RoomState} the copy of the room state
*/
RoomState.prototype.clone = function() {
const copy = new RoomState(this.roomId, this._oobMemberFlags);
// Ugly hack: because setStateEvents will mark
// members as susperseding future out of bound members
// if loading is in progress (through _oobMemberFlags)
// since these are not new members, we're merely copying them
// set the status to not started
// after copying, we set back the status
const status = this._oobMemberFlags.status;
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
Object.values(this.events).forEach((eventsByStateKey) => {
const eventsForType = Object.values(eventsByStateKey);
copy.setStateEvents(eventsForType);
});
// Ugly hack: see above
this._oobMemberFlags.status = status;
if (this._summaryInvitedMemberCount !== null) {
copy.setInvitedMemberCount(this.getInvitedMemberCount());
}
if (this._summaryJoinedMemberCount !== null) {
copy.setJoinedMemberCount(this.getJoinedMemberCount());
}
// copy out of band flags if needed
if (this._oobMemberFlags.status == OOB_STATUS_FINISHED) {
// copy markOutOfBand flags
this.getMembers().forEach((member) => {
if (member.isOutOfBand()) {
const copyMember = copy.getMember(member.userId);
copyMember.markOutOfBand();
}
});
}
return copy;
};
/**
* Add previously unknown state events.
* When lazy loading members while back-paginating,
* the relevant room state for the timeline chunk at the end
* of the chunk can be set with this method.
* @param {MatrixEvent[]} events state events to prepend
*/
RoomState.prototype.setUnknownStateEvents = function(events) {
const unknownStateEvents = events.filter((event) => {
return this.events[event.getType()] === undefined ||
this.events[event.getType()][event.getStateKey()] === undefined;
});
this.setStateEvents(unknownStateEvents);
};
/**
* Add an array of one or more state MatrixEvents, overwriting
* any existing state with the same {type, stateKey} tuple. Will fire
@@ -175,10 +304,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
return;
}
if (self.events[event.getType()] === undefined) {
self.events[event.getType()] = {};
}
self.events[event.getType()][event.getStateKey()] = event;
self._setStateEvent(event);
if (event.getType() === "m.room.member") {
_updateDisplayNameCache(
self, event.getStateKey(), event.getContent().displayname,
@@ -216,24 +342,10 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
event.getPrevContent().displayname;
}
let member = self.members[userId];
if (!member) {
member = new RoomMember(event.getRoomId(), userId);
self.emit("RoomState.newMember", event, self, member);
}
const member = self._getOrCreateMember(userId, event);
member.setMembershipEvent(event, self);
// this member may have a power level already, so set it.
const pwrLvlEvent = self.getStateEvents("m.room.power_levels", "");
if (pwrLvlEvent) {
member.setPowerLevelEvent(pwrLvlEvent);
}
// blow away the sentinel which is now outdated
delete self._sentinels[userId];
self.members[userId] = member;
self._joinedMemberCount = null;
self._updateMember(member);
self.emit("RoomState.members", event, self, member);
} else if (event.getType() === "m.room.power_levels") {
const members = utils.values(self.members);
@@ -248,6 +360,140 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
});
};
/**
* Looks up a member by the given userId, and if it doesn't exist,
* create it and emit the `RoomState.newMember` event.
* This method makes sure the member is added to the members dictionary
* before emitting, as this is done from setStateEvents and _setOutOfBandMember.
* @param {string} userId the id of the user to look up
* @param {MatrixEvent} event the membership event for the (new) member. Used to emit.
* @fires module:client~MatrixClient#event:"RoomState.newMember"
* @returns {RoomMember} the member, existing or newly created.
*/
RoomState.prototype._getOrCreateMember = function(userId, event) {
let member = this.members[userId];
if (!member) {
member = new RoomMember(this.roomId, userId);
// add member to members before emitting any events,
// as event handlers often lookup the member
this.members[userId] = member;
this.emit("RoomState.newMember", event, this, member);
}
return member;
};
RoomState.prototype._setStateEvent = function(event) {
if (this.events[event.getType()] === undefined) {
this.events[event.getType()] = {};
}
this.events[event.getType()][event.getStateKey()] = event;
};
RoomState.prototype._updateMember = function(member) {
// this member may have a power level already, so set it.
const pwrLvlEvent = this.getStateEvents("m.room.power_levels", "");
if (pwrLvlEvent) {
member.setPowerLevelEvent(pwrLvlEvent);
}
// blow away the sentinel which is now outdated
delete this._sentinels[member.userId];
this.members[member.userId] = member;
this._joinedMemberCount = null;
this._invitedMemberCount = null;
};
/**
* Get the out-of-band members loading state, whether loading is needed or not.
* Note that loading might be in progress and hence isn't needed.
* @return {bool} whether or not the members of this room need to be loaded
*/
RoomState.prototype.needsOutOfBandMembers = function() {
return this._oobMemberFlags.status === OOB_STATUS_NOTSTARTED;
};
/**
* Mark this room state as waiting for out-of-band members,
* ensuring it doesn't ask for them to be requested again
* through needsOutOfBandMembers
*/
RoomState.prototype.markOutOfBandMembersStarted = function() {
if (this._oobMemberFlags.status !== OOB_STATUS_NOTSTARTED) {
return;
}
this._oobMemberFlags.status = OOB_STATUS_INPROGRESS;
};
/**
* Mark this room state as having failed to fetch out-of-band members
*/
RoomState.prototype.markOutOfBandMembersFailed = function() {
if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) {
return;
}
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
};
/**
* Clears the loaded out-of-band members
*/
RoomState.prototype.clearOutOfBandMembers = function() {
let count = 0;
Object.keys(this.members).forEach((userId) => {
const member = this.members[userId];
if (member.isOutOfBand()) {
++count;
delete this.members[userId];
}
});
console.log(`LL: RoomState removed ${count} members...`);
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
};
/**
* Sets the loaded out-of-band members.
* @param {MatrixEvent[]} stateEvents array of membership state events
*/
RoomState.prototype.setOutOfBandMembers = function(stateEvents) {
console.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`);
if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) {
return;
}
console.log(`LL: RoomState put in OOB_STATUS_FINISHED state ...`);
this._oobMemberFlags.status = OOB_STATUS_FINISHED;
stateEvents.forEach((e) => this._setOutOfBandMember(e));
};
/**
* Sets a single out of band member, used by both setOutOfBandMembers and clone
* @param {MatrixEvent} stateEvent membership state event
*/
RoomState.prototype._setOutOfBandMember = function(stateEvent) {
if (stateEvent.getType() !== 'm.room.member') {
return;
}
const userId = stateEvent.getStateKey();
const existingMember = this.getMember(userId);
// never replace members received as part of the sync
if (existingMember && !existingMember.isOutOfBand()) {
return;
}
const member = this._getOrCreateMember(userId, stateEvent);
member.setMembershipEvent(stateEvent, this);
// needed to know which members need to be stored seperately
// as they are not part of the sync accumulator
// this is cleared by setMembershipEvent so when it's updated through /sync
member.markOutOfBand();
_updateDisplayNameCache(this, member.userId, member.name);
this._setStateEvent(stateEvent);
this._updateMember(member);
this.emit("RoomState.members", stateEvent, this, member);
};
/**
* Set the current typing event for this room.
* @param {MatrixEvent} event The typing event
@@ -285,12 +531,12 @@ RoomState.prototype.getLastModifiedTime = function() {
};
/**
* Get user IDs with the specified display name.
* Get user IDs with the specified or similar display names.
* @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] || [];
return this._displayNameToUserIds[utils.removeHiddenChars(displayName)] || [];
};
/**
@@ -329,7 +575,7 @@ RoomState.prototype._hasSufficientPowerLevelFor = function(action, powerLevel) {
}
let requiredLevel = 50;
if (powerLevels[action] !== undefined) {
if (utils.isNumber(powerLevels[action])) {
requiredLevel = powerLevels[action];
}
@@ -401,11 +647,6 @@ RoomState.prototype.maySendStateEvent = function(stateEventType, userId) {
* 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;
@@ -413,25 +654,34 @@ RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
let state_default = 0;
let events_default = 0;
let powerLevel = 0;
if (power_levels_event) {
power_levels = power_levels_event.getContent();
events_levels = power_levels.events || {};
if (power_levels.state_default !== undefined) {
if (Number.isFinite(power_levels.state_default)) {
state_default = power_levels.state_default;
} else {
state_default = 50;
}
if (power_levels.events_default !== undefined) {
const userPowerLevel = power_levels.users && power_levels.users[userId];
if (Number.isFinite(userPowerLevel)) {
powerLevel = userPowerLevel;
} else if(Number.isFinite(power_levels.users_default)) {
powerLevel = power_levels.users_default;
}
if (Number.isFinite(power_levels.events_default)) {
events_default = power_levels.events_default;
}
}
let required_level = state ? state_default : events_default;
if (events_levels[eventType] !== undefined) {
if (Number.isFinite(events_levels[eventType])) {
required_level = events_levels[eventType];
}
return member.powerLevel >= required_level;
return powerLevel >= required_level;
};
/**
@@ -455,7 +705,7 @@ RoomState.prototype.mayTriggerNotifOfType = function(notifLevelKey, userId) {
powerLevelsEvent &&
powerLevelsEvent.getContent() &&
powerLevelsEvent.getContent().notifications &&
powerLevelsEvent.getContent().notifications[notifLevelKey]
utils.isNumber(powerLevelsEvent.getContent().notifications[notifLevelKey])
) {
notifLevel = powerLevelsEvent.getContent().notifications[notifLevelKey];
}
@@ -494,22 +744,26 @@ function _updateDisplayNameCache(roomState, userId, displayName) {
// 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--;
}
const strippedOldName = utils.removeHiddenChars(oldName);
const existingUserIds = roomState._displayNameToUserIds[strippedOldName];
if (existingUserIds) {
// remove this user ID from this array
const filteredUserIDs = existingUserIds.filter((id) => id !== userId);
roomState._displayNameToUserIds[strippedOldName] = filteredUserIDs;
}
roomState._displayNameToUserIds[oldName] = existingUserIds;
}
roomState._userIdsToDisplayNames[userId] = displayName;
if (!roomState._displayNameToUserIds[displayName]) {
roomState._displayNameToUserIds[displayName] = [];
const strippedDisplayname = displayName && utils.removeHiddenChars(displayName);
// an empty stripped displayname (undefined/'') will be set to MXID in room-member.js
if (strippedDisplayname) {
if (!roomState._displayNameToUserIds[strippedDisplayname]) {
roomState._displayNameToUserIds[strippedDisplayname] = [];
}
roomState._displayNameToUserIds[strippedDisplayname].push(userId);
}
roomState._displayNameToUserIds[displayName].push(userId);
}
/**
@@ -539,7 +793,8 @@ function _updateDisplayNameCache(roomState, userId, displayName) {
/**
* Fires whenever a member is added to the members dictionary. The RoomMember
* will not be fully populated yet (e.g. no membership state).
* will not be fully populated yet (e.g. no membership state) but will already
* be available in the members dictionary.
* @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
+567 -91
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -21,6 +22,7 @@ const EventEmitter = require("events").EventEmitter;
const EventStatus = require("./event").EventStatus;
const RoomSummary = require("./room-summary");
const RoomMember = require("./room-member");
const MatrixEvent = require("./event").MatrixEvent;
const utils = require("../utils");
const ContentRepo = require("../content-repo");
@@ -29,6 +31,15 @@ const EventTimelineSet = require("./event-timeline-set");
import ReEmitter from '../ReEmitter';
// These constants are used as sane defaults when the homeserver doesn't support
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
// the same as the common default room version whereas SAFE_ROOM_VERSIONS are the
// room versions which are considered okay for people to run without being asked
// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
// return an m.room_versions capability.
const KNOWN_SAFE_ROOM_VERSION = '1';
const SAFE_ROOM_VERSIONS = ['1', '2'];
function synthesizeReceipt(userId, event, receiptType) {
// console.log("synthesizing receipt for "+event.getId());
// This is really ugly because JS has no way to express an object literal
@@ -68,6 +79,8 @@ function synthesizeReceipt(userId, event, receiptType) {
* @constructor
* @alias module:models/room
* @param {string} roomId Required. The ID of this room.
* @param {MatrixClient} client Required. The client, used to lazy load members.
* @param {string} myUserId Required. The ID of the syncing user.
* @param {Object=} opts Configuration options
* @param {*} opts.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
@@ -94,15 +107,15 @@ function synthesizeReceipt(userId, event, receiptType) {
* event type and the values are the events.
* @prop {RoomState} oldState The state of the room at the time of the oldest
* event in the live timeline. Present for backwards compatibility -
* prefer getLiveTimeline().getState(true).
* prefer getLiveTimeline().getState(EventTimeline.BACKWARDS).
* @prop {RoomState} currentState The state of the room at the time of the
* newest event in the timeline. Present for backwards compatibility -
* prefer getLiveTimeline().getState(false).
* prefer getLiveTimeline().getState(EventTimeline.FORWARDS).
* @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, opts) {
function Room(roomId, client, myUserId, opts) {
opts = opts || {};
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
@@ -115,6 +128,7 @@ function Room(roomId, opts) {
);
}
this.myUserId = myUserId;
this.roomId = roomId;
this.name = roomId;
this.tags = {
@@ -171,9 +185,118 @@ function Room(roomId, opts) {
// read by megolm; boolean value - null indicates "use global value"
this._blacklistUnverifiedDevices = null;
this._selfMembership = null;
this._summaryHeroes = null;
// awaited by getEncryptionTargetMembers while room members are loading
this._client = client;
if (!this._opts.lazyLoadMembers) {
this._membersPromise = Promise.resolve();
} else {
this._membersPromise = null;
}
}
utils.inherits(Room, EventEmitter);
/**
* Gets the version of the room
* @returns {string} The version of the room, or null if it could not be determined
*/
Room.prototype.getVersion = function() {
const createEvent = this.currentState.getStateEvents("m.room.create", "");
if (!createEvent) {
console.warn("Room " + this.room_id + " does not have an m.room.create event");
return '1';
}
const ver = createEvent.getContent()['room_version'];
if (ver === undefined) return '1';
return ver;
};
/**
* Determines whether this room needs to be upgraded to a new version
* @returns {string?} What version the room should be upgraded to, or null if
* the room does not require upgrading at this time.
* @deprecated Use #getRecommendedVersion() instead
*/
Room.prototype.shouldUpgradeToVersion = function() {
// TODO: Remove this function.
// This makes assumptions about which versions are safe, and can easily
// be wrong. Instead, people are encouraged to use getRecommendedVersion
// which determines a safer value. This function doesn't use that function
// because this is not async-capable, and to avoid breaking the contract
// we're deprecating this.
if (!SAFE_ROOM_VERSIONS.includes(this.getVersion())) {
return KNOWN_SAFE_ROOM_VERSION;
}
return null;
};
/**
* Determines the recommended room version for the room. This returns an
* object with 3 properties: <code>version</code> as the new version the
* room should be upgraded to (may be the same as the current version);
* <code>needsUpgrade</code> to indicate if the room actually can be
* upgraded (ie: does the current version not match?); and <code>urgent</code>
* to indicate if the new version patches a vulnerability in a previous
* version.
* @returns {Promise<{version: string, needsUpgrade: bool, urgent: bool}>}
* Resolves to the version the room should be upgraded to.
*/
Room.prototype.getRecommendedVersion = async function() {
const capabilities = await this._client.getCapabilities();
let versionCap = capabilities["m.room_versions"];
if (!versionCap) {
versionCap = {
default: KNOWN_SAFE_ROOM_VERSION,
available: {},
};
for (const safeVer of SAFE_ROOM_VERSIONS) {
versionCap.available[safeVer] = "stable";
}
}
const currentVersion = this.getVersion();
const result = {
version: currentVersion,
needsUpgrade: false,
urgent: false,
};
// If the room is on the default version then nothing needs to change
if (currentVersion === versionCap.default) return Promise.resolve(result);
const stableVersions = Object.keys(versionCap.available)
.filter((v) => versionCap.available[v] === 'stable');
// Check if the room is on an unstable version. We determine urgency based
// off the version being in the Matrix spec namespace or not (if the version
// is in the current namespace and unstable, the room is probably vulnerable).
if (!stableVersions.includes(currentVersion)) {
result.version = versionCap.default;
result.needsUpgrade = true;
result.urgent = !!this.getVersion().match(/^[0-9]+[0-9.]*$/g);
return Promise.resolve(result);
}
// The room is on a stable, but non-default, version by this point.
// No upgrade needed.
return Promise.resolve(result);
};
/**
* Determines whether the given user is permitted to perform a room upgrade
* @param {String} userId The ID of the user to test against
* @returns {bool} True if the given user is permitted to upgrade the room
*/
Room.prototype.userMayUpgradeRoom = function(userId) {
return this.currentState.maySendStateEvent("m.room.tombstone", userId);
};
/**
* Get the list of pending sent events for this room
*
@@ -201,6 +324,232 @@ Room.prototype.getLiveTimeline = function() {
return this.getUnfilteredTimelineSet().getLiveTimeline();
};
/**
* @param {string} myUserId the user id for the logged in member
* @return {string} the membership type (join | leave | invite) for the logged in user
*/
Room.prototype.getMyMembership = function() {
return this._selfMembership;
};
/**
* If this room is a DM we're invited to,
* try to find out who invited us
* @return {string} user id of the inviter
*/
Room.prototype.getDMInviter = function() {
if (this.myUserId) {
const me = this.getMember(this.myUserId);
if (me) {
return me.getDMInviter();
}
}
if (this._selfMembership === "invite") {
// fall back to summary information
const memberCount = this.getInvitedAndJoinedMemberCount();
if (memberCount == 2 && this._summaryHeroes.length) {
return this._summaryHeroes[0];
}
}
};
/**
* Assuming this room is a DM room, tries to guess with which user.
* @return {string} user id of the other member (could be syncing user)
*/
Room.prototype.guessDMUserId = function() {
const me = this.getMember(this.myUserId);
if (me) {
const inviterId = me.getDMInviter();
if (inviterId) {
return inviterId;
}
}
// remember, we're assuming this room is a DM,
// so returning the first member we find should be fine
const hasHeroes = Array.isArray(this._summaryHeroes) &&
this._summaryHeroes.length;
if (hasHeroes) {
return this._summaryHeroes[0];
}
const members = this.currentState.getMembers();
const anyMember = members.find((m) => m.userId !== this.myUserId);
if (anyMember) {
return anyMember.userId;
}
// it really seems like I'm the only user in the room
// so I probably created a room with just me in it
// and marked it as a DM. Ok then
return this.myUserId;
};
Room.prototype.getAvatarFallbackMember = function() {
const memberCount = this.getInvitedAndJoinedMemberCount();
if (memberCount > 2) {
return;
}
const hasHeroes = Array.isArray(this._summaryHeroes) &&
this._summaryHeroes.length;
if (hasHeroes) {
const availableMember = this._summaryHeroes.map((userId) => {
return this.getMember(userId);
}).find((member) => !!member);
if (availableMember) {
return availableMember;
}
}
const members = this.currentState.getMembers();
// could be different than memberCount
// as this includes left members
if (members.length <= 2) {
const availableMember = members.find((m) => {
return m.userId !== this.myUserId;
});
if (availableMember) {
return availableMember;
}
}
// if all else fails, try falling back to a user,
// and create a one-off member for it
if (hasHeroes) {
const availableUser = this._summaryHeroes.map((userId) => {
return this._client.getUser(userId);
}).find((user) => !!user);
if (availableUser) {
const member = new RoomMember(
this.roomId, availableUser.userId);
member.user = availableUser;
return member;
}
}
};
/**
* Sets the membership this room was received as during sync
* @param {string} membership join | leave | invite
*/
Room.prototype.updateMyMembership = function(membership) {
const prevMembership = this._selfMembership;
this._selfMembership = membership;
if (prevMembership !== membership) {
if (membership === "leave") {
this._cleanupAfterLeaving();
}
this.emit("Room.myMembership", this, membership, prevMembership);
}
};
Room.prototype._loadMembersFromServer = async function() {
const lastSyncToken = this._client.store.getSyncToken();
const queryString = utils.encodeParams({
not_membership: "leave",
at: lastSyncToken,
});
const path = utils.encodeUri("/rooms/$roomId/members?" + queryString,
{$roomId: this.roomId});
const http = this._client._http;
const response = await http.authedRequest(undefined, "GET", path);
return response.chunk;
};
Room.prototype._loadMembers = async function() {
// were the members loaded from the server?
let fromServer = false;
let rawMembersEvents =
await this._client.store.getOutOfBandMembers(this.roomId);
if (rawMembersEvents === null) {
fromServer = true;
rawMembersEvents = await this._loadMembersFromServer();
console.log(`LL: got ${rawMembersEvents.length} ` +
`members from server for room ${this.roomId}`);
}
const memberEvents = rawMembersEvents.map(this._client.getEventMapper());
return {memberEvents, fromServer};
};
/**
* Preloads the member list in case lazy loading
* of memberships is in use. Can be called multiple times,
* it will only preload once.
* @return {Promise} when preloading is done and
* accessing the members on the room will take
* all members in the room into account
*/
Room.prototype.loadMembersIfNeeded = function() {
if (this._membersPromise) {
return this._membersPromise;
}
// mark the state so that incoming messages while
// the request is in flight get marked as superseding
// the OOB members
this.currentState.markOutOfBandMembersStarted();
const inMemoryUpdate = this._loadMembers().then((result) => {
this.currentState.setOutOfBandMembers(result.memberEvents);
// now the members are loaded, start to track the e2e devices if needed
if (this._client.isRoomEncrypted(this.roomId)) {
this._client._crypto.trackRoomDevices(this.roomId);
}
return result.fromServer;
}).catch((err) => {
// allow retries on fail
this._membersPromise = null;
this.currentState.markOutOfBandMembersFailed();
throw err;
});
// update members in storage, but don't wait for it
inMemoryUpdate.then((fromServer) => {
if (fromServer) {
const oobMembers = this.currentState.getMembers()
.filter((m) => m.isOutOfBand())
.map((m) => m.events.member.event);
console.log(`LL: telling store to write ${oobMembers.length}`
+ ` members for room ${this.roomId}`);
const store = this._client.store;
return store.setOutOfBandMembers(this.roomId, oobMembers)
// swallow any IDB error as we don't want to fail
// because of this
.catch((err) => {
console.log("LL: storing OOB room members failed, oh well",
err);
});
}
}).catch((err) => {
// as this is not awaited anywhere,
// at least show the error in the console
console.error(err);
});
this._membersPromise = inMemoryUpdate;
return this._membersPromise;
};
/**
* Removes the lazily loaded members from storage if needed
*/
Room.prototype.clearLoadedMembersIfNeeded = async function() {
if (this._opts.lazyLoadMembers && this._membersPromise) {
await this.loadMembersIfNeeded();
await this._client.store.clearOutOfBandMembers(this.roomId);
this.currentState.clearOutOfBandMembers();
this._membersPromise = null;
}
};
/**
* called when sync receives this room in the leave section
* to do cleanup after leaving a room. Possibly called multiple times.
*/
Room.prototype._cleanupAfterLeaving = function() {
this.clearLoadedMembersIfNeeded().catch((err) => {
console.error(`error after clearing loaded members from ` +
`room ${this.roomId} after leaving`);
console.dir(err);
});
};
/**
* Reset the live timeline of all timelineSets, and start new ones.
@@ -240,6 +589,24 @@ Room.prototype._fixUpLegacyTimelineFields = function() {
.getState(EventTimeline.FORWARDS);
};
/**
* Returns whether there are any devices in the room that are unverified
* @return {bool} the result
*/
Room.prototype.hasUnverifiedDevices = async function() {
if (!this._client.isRoomEncrypted(this.roomId)) {
return false;
}
const e2eMembers = await this.getEncryptionTargetMembers();
for (const member of e2eMembers) {
const devices = await this._client.getStoredDevicesForUser(member.userId);
if (devices.some((device) => device.isUnverified())) {
return true;
}
}
return false;
};
/**
* Return the timeline sets for this room.
* @return {EventTimelineSet[]} array of timeline sets for this room
@@ -306,6 +673,26 @@ Room.prototype.setUnreadNotificationCount = function(type, count) {
this._notificationCounts[type] = count;
};
Room.prototype.setSummary = function(summary) {
const heroes = summary["m.heroes"];
const joinedCount = summary["m.joined_member_count"];
const invitedCount = summary["m.invited_member_count"];
if (Number.isInteger(joinedCount)) {
this.currentState.setJoinedMemberCount(joinedCount);
}
if (Number.isInteger(invitedCount)) {
this.currentState.setInvitedMemberCount(invitedCount);
}
if (Array.isArray(heroes)) {
// be cautious about trusting server values,
// and make sure heroes doesn't contain our own id
// just to be sure
this._summaryHeroes = heroes.filter((userId) => {
return userId !== this.myUserId;
});
}
};
/**
* Whether to send encrypted messages to devices within this room.
* @param {Boolean} value true to blacklist unverified devices, null
@@ -430,11 +817,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
* @return {RoomMember} The member or <code>null</code>.
*/
Room.prototype.getMember = function(userId) {
const member = this.currentState.members[userId];
if (!member) {
return null;
}
return member;
return this.currentState.getMember(userId);
};
/**
@@ -445,6 +828,33 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
return this.getMembersWithMembership("join");
};
/**
* Returns the number of joined members in this room
* This method caches the result.
* This is a wrapper around the method of the same name in roomState, returning
* its result for the room's current state.
* @return {integer} The number of members in this room whose membership is 'join'
*/
Room.prototype.getJoinedMemberCount = function() {
return this.currentState.getJoinedMemberCount();
};
/**
* Returns the number of invited members in this room
* @return {integer} The number of members in this room whose membership is 'invite'
*/
Room.prototype.getInvitedMemberCount = function() {
return this.currentState.getInvitedMemberCount();
};
/**
* Returns the number of invited + joined members in this room
* @return {integer} The number of members in this room whose membership is 'invite' or 'join'
*/
Room.prototype.getInvitedAndJoinedMemberCount = function() {
return this.getInvitedMemberCount() + this.getJoinedMemberCount();
};
/**
* Get a list of members with given membership state.
* @param {string} membership The membership state.
@@ -456,6 +866,29 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
});
};
/**
* Get a list of members we should be encrypting for in this room
* @return {Promise<RoomMember[]>} A list of members who
* we should encrypt messages for in this room.
*/
Room.prototype.getEncryptionTargetMembers = async function() {
await this.loadMembersIfNeeded();
let members = this.getMembersWithMembership("join");
if (this.shouldEncryptForInvitedMembers()) {
members = members.concat(this.getMembersWithMembership("invite"));
}
return members;
};
/**
* Determine whether we should encrypt messages for invited users in this room
* @return {boolean} if we should encrypt messages for invited users
*/
Room.prototype.shouldEncryptForInvitedMembers = function() {
const ev = this.currentState.getStateEvents("m.room.history_visibility", "");
return (ev && ev.getContent() && ev.getContent().history_visibility !== "joined");
};
/**
* Get the default room name (i.e. what a given user would see if the
* room had no m.room.name)
@@ -653,6 +1086,10 @@ Room.prototype.addPendingEvent = function(event, txnId) {
this._txnToEvent[txnId] = event;
if (this._opts.pendingEventOrdering == "detached") {
if (this._pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
console.warn("Setting event as NOT_SENT due to messages in the same state");
event.status = EventStatus.NOT_SENT;
}
this._pendingEventList.push(event);
} else {
for (let i = 0; i < this._timelineSets.length; i++) {
@@ -817,7 +1254,7 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
this.removeEvent(oldEventId);
}
this.emit("Room.localEchoUpdated", event, this, event.getId(), oldStatus);
this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus);
};
@@ -909,15 +1346,14 @@ Room.prototype.removeEvent = function(eventId) {
* 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) {
Room.prototype.recalculate = function() {
// set fake stripped state events if this is an invite room so logic remains
// consistent elsewhere.
const self = this;
const membershipEvent = this.currentState.getStateEvents(
"m.room.member", userId,
"m.room.member", this.myUserId,
);
if (membershipEvent && membershipEvent.getContent().membership === "invite") {
const strippedStateEvents = membershipEvent.event.invite_room_state || [];
@@ -933,14 +1369,14 @@ Room.prototype.recalculate = function(userId) {
content: strippedEvent.content,
event_id: "$fake" + Date.now(),
room_id: self.roomId,
user_id: userId, // technically a lie
user_id: self.myUserId, // technically a lie
})]);
}
});
}
const oldName = this.name;
this.name = calculateRoomName(this, userId);
this.name = calculateRoomName(this, this.myUserId);
this.summary = new RoomSummary(this.roomId, {
title: this.name,
});
@@ -950,7 +1386,6 @@ Room.prototype.recalculate = function(userId) {
}
};
/**
* Get a list of user IDs who have <b>read up to</b> the given event.
* @param {MatrixEvent} event the event to get read receipts for.
@@ -989,6 +1424,40 @@ Room.prototype.getEventReadUpTo = function(userId, ignoreSynthesized) {
return receipts["m.read"][userId].eventId;
};
/**
* Determines if the given user has read a particular event ID with the known
* history of the room. This is not a definitive check as it relies only on
* what is available to the room at the time of execution.
* @param {String} userId The user ID to check the read state of.
* @param {String} eventId The event ID to check if the user read.
* @returns {Boolean} True if the user has read the event, false otherwise.
*/
Room.prototype.hasUserReadEvent = function(userId, eventId) {
const readUpToId = this.getEventReadUpTo(userId, false);
if (readUpToId === eventId) return true;
if (this.timeline.length
&& this.timeline[this.timeline.length - 1].getSender()
&& this.timeline[this.timeline.length - 1].getSender() === userId) {
// It doesn't matter where the event is in the timeline, the user has read
// it because they've sent the latest event.
return true;
}
for (let i = this.timeline.length - 1; i >= 0; --i) {
const ev = this.timeline[i];
// If we encounter the target event first, the user hasn't read it
// however if we encounter the readUpToId first then the user has read
// it. These rules apply because we're iterating bottom-up.
if (ev.getId() === eventId) return false;
if (ev.getId() === readUpToId) return true;
}
// We don't know if the user has read it, so assume not.
return false;
};
/**
* Get a list of receipts for the given event.
* @param {MatrixEvent} event the event to get receipts for
@@ -1122,7 +1591,7 @@ Room.prototype.addTags = function(event) {
// }
// XXX: do we need to deep copy here?
this.tags = event.getContent().tags;
this.tags = event.getContent().tags || {};
// XXX: we could do a deep-comparison to see if the tags have really
// changed - but do we want to bother?
@@ -1153,6 +1622,17 @@ Room.prototype.getAccountData = function(type) {
return this.accountData[type];
};
/**
* Returns wheter the syncing user has permission to send a message in the room
* @return {boolean} true if the user should be permitted to send
* message events into the room.
*/
Room.prototype.maySendMessage = function() {
return this.getMyMembership() === 'join' &&
this.currentState.maySendEvent('m.room.message', this.myUserId);
};
/**
* This is an internal method. Calculates the name of the room from the current
* room state.
@@ -1186,87 +1666,83 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
return alias;
}
// get members that are NOT ourselves and are actually in the room.
const otherMembers = utils.filter(room.currentState.getMembers(), function(m) {
return (
m.userId !== userId && m.membership !== "leave" && m.membership !== "ban"
);
});
const allMembers = utils.filter(room.currentState.getMembers(), function(m) {
return (m.membership !== "leave");
});
const myMemberEventArray = utils.filter(room.currentState.getMembers(), function(m) {
return (m.userId == userId);
});
const myMemberEvent = (
(myMemberEventArray.length && myMemberEventArray[0].events) ?
myMemberEventArray[0].events.member.event : undefined
);
const joinedMemberCount = room.currentState.getJoinedMemberCount();
const invitedMemberCount = room.currentState.getInvitedMemberCount();
// -1 because these numbers include the syncing user
const inviteJoinCount = joinedMemberCount + invitedMemberCount - 1;
// TODO: Localisation
if (myMemberEvent && myMemberEvent.content.membership == "invite") {
if (room.currentState.getMember(myMemberEvent.sender)) {
// extract who invited us to the room
return room.currentState.getMember(
myMemberEvent.sender,
).name;
} else if (allMembers[0].events.member) {
// use the sender field from the invite event, although this only
// gets us the mxid
return myMemberEvent.sender;
} else {
return "Room Invite";
}
// get members that are NOT ourselves and are actually in the room.
let otherNames = null;
if (room._summaryHeroes) {
// if we have a summary, the member state events
// should be in the room state
otherNames = room._summaryHeroes.map((userId) => {
const member = room.getMember(userId);
return member ? member.name : userId;
});
} else {
let otherMembers = room.currentState.getMembers().filter((m) => {
return m.userId !== userId &&
(m.membership === "invite" || m.membership === "join");
});
// make sure members have stable order
otherMembers.sort((a, b) => a.userId.localeCompare(b.userId));
// only 5 first members, immitate _summaryHeroes
otherMembers = otherMembers.slice(0, 5);
otherNames = otherMembers.map((m) => m.name);
}
if (inviteJoinCount) {
return memberNamesToRoomName(otherNames, inviteJoinCount);
}
if (otherMembers.length === 0) {
const leftMembers = utils.filter(room.currentState.getMembers(), function(m) {
return m.userId !== userId && m.membership === "leave";
});
if (allMembers.length === 1) {
// self-chat, peeked room with 1 participant,
// or inbound invite, or outbound 3PID invite.
if (allMembers[0].userId === userId) {
const thirdPartyInvites =
room.currentState.getStateEvents("m.room.third_party_invite");
if (thirdPartyInvites && thirdPartyInvites.length > 0) {
let name = "Inviting " +
thirdPartyInvites[0].getContent().display_name;
if (thirdPartyInvites.length > 1) {
if (thirdPartyInvites.length == 2) {
name += " and " +
thirdPartyInvites[1].getContent().display_name;
} else {
name += " and " +
thirdPartyInvites.length + " others";
}
}
return name;
} else if (leftMembers.length === 1) {
// if it was a chat with one person who's now left, it's still
// notionally a chat with them
return leftMembers[0].name;
} else {
return "Empty room";
}
} else {
return allMembers[0].name;
}
} else {
// there really isn't anyone in this room...
return "Empty room";
const myMembership = room.getMyMembership();
// if I have created a room and invited people throuh
// 3rd party invites
if (myMembership == 'join') {
const thirdPartyInvites =
room.currentState.getStateEvents("m.room.third_party_invite");
if (thirdPartyInvites && thirdPartyInvites.length) {
const thirdPartyNames = thirdPartyInvites.map((i) => {
return i.getContent().display_name;
});
return `Inviting ${memberNamesToRoomName(thirdPartyNames)}`;
}
} else if (otherMembers.length === 1) {
return otherMembers[0].name;
} else if (otherMembers.length === 2) {
return (
otherMembers[0].name + " and " + otherMembers[1].name
);
}
// let's try to figure out who was here before
let leftNames = otherNames;
// if we didn't have heroes, try finding them in the room state
if(!leftNames.length) {
leftNames = room.currentState.getMembers().filter((m) => {
return m.userId !== userId &&
m.membership !== "invite" &&
m.membership !== "join";
}).map((m) => m.name);
}
if(leftNames.length) {
return `Empty room (was ${memberNamesToRoomName(leftNames)})`;
} else {
return (
otherMembers[0].name + " and " + (otherMembers.length - 1) + " others"
);
return "Empty room";
}
}
function memberNamesToRoomName(names, count = (names.length + 1)) {
const countWithoutMe = count - 1;
if (!names.length) {
return "Empty room";
} else if (names.length === 1 && countWithoutMe <= 1) {
return names[0];
} else if (names.length === 2 && countWithoutMe <= 2) {
return `${names[0]} and ${names[1]}`;
} else {
const plural = countWithoutMe > 1;
if (plural) {
return `${names[0]} and ${countWithoutMe} others`;
} else {
return `${names[0]} and 1 other`;
}
}
}
+16
View File
@@ -39,6 +39,9 @@ limitations under the License.
* 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 {string} _unstable_statusMessage The status message for the user, if known. This is
* different from the presenceStatusMsg in that this is not tied to
* the user's presence, and should be represented differently.
* @prop {Object} events The events describing this user.
* @prop {MatrixEvent} events.presence The m.presence event for this user.
*/
@@ -46,6 +49,7 @@ function User(userId) {
this.userId = userId;
this.presence = "offline";
this.presenceStatusMsg = null;
this._unstable_statusMessage = "";
this.displayName = userId;
this.rawDisplayName = userId;
this.avatarUrl = null;
@@ -179,6 +183,18 @@ User.prototype.getLastActiveTs = function() {
return this.lastPresenceTs - this.lastActiveAgo;
};
/**
* Manually set the user's status message.
* @param {MatrixEvent} event The <code>im.vector.user_status</code> event.
* @fires module:client~MatrixClient#event:"User._unstable_statusMessage"
*/
User.prototype._unstable_updateStatusMessage = function(event) {
if (!event.getContent()) this._unstable_statusMessage = "";
else this._unstable_statusMessage = event.getContent()["status"];
this._updateModifiedTime();
this.emit("User._unstable_statusMessage", this);
};
/**
* The User class.
*/
+7 -21
View File
@@ -14,6 +14,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {escapeRegExp, globToRegexp} from "./utils";
/**
* @module pushprocessor
*/
@@ -26,10 +29,6 @@ const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'
* @param {Object} client The Matrix client object to use
*/
function PushProcessor(client) {
const escapeRegExp = function(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
const cachedGlobToRegex = {
// $glob: RegExp,
};
@@ -185,7 +184,10 @@ function PushProcessor(client) {
};
const eventFulfillsDisplayNameCondition = function(cond, ev) {
const content = ev.getContent();
let content = ev.getContent();
if (ev.isEncrypted() && ev.getClearContent()) {
content = ev.getClearContent();
}
if (!content || !content.body || typeof content.body != 'string') {
return false;
}
@@ -244,22 +246,6 @@ function PushProcessor(client) {
return cachedGlobToRegex[glob];
};
const globToRegexp = function(glob) {
// From
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
// Because micromatch is about 130KB with dependencies,
// and minimatch is not much better.
let pat = escapeRegExp(glob);
pat = pat.replace(/\\\*/g, '.*');
pat = pat.replace(/\?/g, '.');
pat = pat.replace(/\\\[(!|)(.*)\\]/g, 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;
+26
View File
@@ -0,0 +1,26 @@
/*
Copyright 2018 New Vector 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.
*/
export function randomString(len) {
let ret = "";
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < len; ++i) {
ret += chars.charAt(Math.floor(Math.random() * chars.length));
}
return ret;
}
+186 -9
View File
@@ -19,7 +19,7 @@ import Promise from 'bluebird';
import SyncAccumulator from "../sync-accumulator";
import utils from "../utils";
const VERSION = 1;
const VERSION = 3;
function createDatabase(db) {
// Make user store, clobber based on user ID. (userId property of User objects)
@@ -33,6 +33,20 @@ function createDatabase(db) {
db.createObjectStore("sync", { keyPath: ["clobber"] });
}
function upgradeSchemaV2(db) {
const oobMembersStore = db.createObjectStore(
"oob_membership_events", {
keyPath: ["room_id", "state_key"],
});
oobMembersStore.createIndex("room", "room_id");
}
function upgradeSchemaV3(db) {
db.createObjectStore("client_options",
{ keyPath: ["clobber"]});
}
/**
* Helper method to collect results from a Cursor and promiseify it.
* @param {ObjectStore|Index} store The store to perform openCursor on.
@@ -63,28 +77,39 @@ function selectQuery(store, keyRange, resultMapper) {
});
}
function promiseifyTxn(txn) {
function txnAsPromise(txn) {
return new Promise((resolve, reject) => {
txn.oncomplete = function(event) {
resolve(event);
};
txn.onerror = function(event) {
reject(event);
reject(event.target.error);
};
});
}
function promiseifyRequest(req) {
function reqAsEventPromise(req) {
return new Promise((resolve, reject) => {
req.onsuccess = function(event) {
resolve(event);
};
req.onerror = function(event) {
reject(event);
reject(event.target.error);
};
});
}
function reqAsPromise(req) {
return new Promise((resolve, reject) => {
req.onsuccess = () => resolve(req);
req.onerror = (err) => reject(err);
});
}
function reqAsCursorPromise(req) {
return reqAsEventPromise(req).then((event) => event.target.result);
}
/**
* Does the actual reading from and writing to the indexeddb
*
@@ -104,6 +129,7 @@ const LocalIndexedDBStoreBackend = function LocalIndexedDBStoreBackend(
this.db = null;
this._disconnected = true;
this._syncAccumulator = new SyncAccumulator();
this._isNewlyCreated = false;
};
@@ -134,8 +160,15 @@ LocalIndexedDBStoreBackend.prototype = {
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
);
if (oldVersion < 1) { // The database did not previously exist.
this._isNewlyCreated = true;
createDatabase(db);
}
if (oldVersion < 2) {
upgradeSchemaV2(db);
}
if (oldVersion < 3) {
upgradeSchemaV3(db);
}
// Expand as needed.
};
@@ -148,7 +181,7 @@ LocalIndexedDBStoreBackend.prototype = {
console.log(
`LocalIndexedDBStoreBackend.connect: awaiting connection...`,
);
return promiseifyRequest(req).then((ev) => {
return reqAsEventPromise(req).then((ev) => {
console.log(
`LocalIndexedDBStoreBackend.connect: connected`,
);
@@ -163,6 +196,10 @@ LocalIndexedDBStoreBackend.prototype = {
return this._init();
});
},
/** @return {bool} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
return Promise.resolve(this._isNewlyCreated);
},
/**
* Having connected, load initial data from the database and prepare for use
@@ -187,6 +224,124 @@ LocalIndexedDBStoreBackend.prototype = {
});
},
/**
* Returns the out-of-band membership events for this room that
* were previously loaded.
* @param {string} roomId
* @returns {Promise<event[]>} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
getOutOfBandMembers: function(roomId) {
return new Promise((resolve, reject) =>{
const tx = this.db.transaction(["oob_membership_events"], "readonly");
const store = tx.objectStore("oob_membership_events");
const roomIndex = store.index("room");
const range = IDBKeyRange.only(roomId);
const request = roomIndex.openCursor(range);
const membershipEvents = [];
// did we encounter the oob_written marker object
// amongst the results? That means OOB member
// loading already happened for this room
// but there were no members to persist as they
// were all known already
let oobWritten = false;
request.onsuccess = (event) => {
const cursor = event.target.result;
if (!cursor) {
// Unknown room
if (!membershipEvents.length && !oobWritten) {
return resolve(null);
}
return resolve(membershipEvents);
}
const record = cursor.value;
if (record.oob_written) {
oobWritten = true;
} else {
membershipEvents.push(record);
}
cursor.continue();
};
request.onerror = (err) => {
reject(err);
};
}).then((events) => {
console.log(`LL: got ${events && events.length}` +
` membershipEvents from storage for room ${roomId} ...`);
return events;
});
},
/**
* Stores the out-of-band membership events for this room. Note that
* it still makes sense to store an empty array as the OOB status for the room is
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
* @param {string} roomId
* @param {event[]} membershipEvents the membership events to store
*/
setOutOfBandMembers: async function(roomId, membershipEvents) {
console.log(`LL: backend about to store ${membershipEvents.length}` +
` members for ${roomId}`);
const tx = this.db.transaction(["oob_membership_events"], "readwrite");
const store = tx.objectStore("oob_membership_events");
membershipEvents.forEach((e) => {
store.put(e);
});
// aside from all the events, we also write a marker object to the store
// to mark the fact that OOB members have been written for this room.
// It's possible that 0 members need to be written as all where previously know
// but we still need to know whether to return null or [] from getOutOfBandMembers
// where null means out of band members haven't been stored yet for this room
const markerObject = {
room_id: roomId,
oob_written: true,
state_key: 0,
};
store.put(markerObject);
await txnAsPromise(tx);
console.log(`LL: backend done storing for ${roomId}!`);
},
clearOutOfBandMembers: async function(roomId) {
// the approach to delete all members for a room
// is to get the min and max state key from the index
// for that room, and then delete between those
// keys in the store.
// this should be way faster than deleting every member
// individually for a large room.
const readTx = this.db.transaction(
["oob_membership_events"],
"readonly");
const store = readTx.objectStore("oob_membership_events");
const roomIndex = store.index("room");
const roomRange = IDBKeyRange.only(roomId);
const minStateKeyProm = reqAsCursorPromise(
roomIndex.openKeyCursor(roomRange, "next"),
).then((cursor) => cursor && cursor.primaryKey[1]);
const maxStateKeyProm = reqAsCursorPromise(
roomIndex.openKeyCursor(roomRange, "prev"),
).then((cursor) => cursor && cursor.primaryKey[1]);
const [minStateKey, maxStateKey] = await Promise.all(
[minStateKeyProm, maxStateKeyProm]);
const writeTx = this.db.transaction(
["oob_membership_events"],
"readwrite");
const writeStore = writeTx.objectStore("oob_membership_events");
const membersKeyRange = IDBKeyRange.bound(
[roomId, minStateKey],
[roomId, maxStateKey],
);
console.log(`LL: Deleting all users + marker in storage for ` +
`room ${roomId}, with key range:`,
[roomId, minStateKey], [roomId, maxStateKey]);
await reqAsPromise(writeStore.delete(membersKeyRange));
},
/**
* Clear the entire database. This should be used when logging out of a client
* to prevent mixing data between accounts.
@@ -284,7 +439,7 @@ LocalIndexedDBStoreBackend.prototype = {
roomsData: roomsData,
groupsData: groupsData,
}); // put == UPSERT
return promiseifyTxn(txn);
return txnAsPromise(txn);
});
},
@@ -301,7 +456,7 @@ LocalIndexedDBStoreBackend.prototype = {
for (let i = 0; i < accountData.length; i++) {
store.put(accountData[i]); // put == UPSERT
}
return promiseifyTxn(txn);
return txnAsPromise(txn);
});
},
@@ -323,7 +478,7 @@ LocalIndexedDBStoreBackend.prototype = {
event: tuple[1],
}); // put == UPSERT
}
return promiseifyTxn(txn);
return txnAsPromise(txn);
});
},
@@ -389,6 +544,28 @@ LocalIndexedDBStoreBackend.prototype = {
});
});
},
getClientOptions: function() {
return Promise.resolve().then(() => {
const txn = this.db.transaction(["client_options"], "readonly");
const store = txn.objectStore("client_options");
return selectQuery(store, undefined, (cursor) => {
if (cursor.value && cursor.value && cursor.value.options) {
return cursor.value.options;
}
}).then((results) => results[0]);
});
},
storeClientOptions: async function(options) {
const txn = this.db.transaction(["client_options"], "readwrite");
const store = txn.objectStore("client_options");
store.put({
clobber: "-", // constant key so will always clobber
options: options,
}); // put == UPSERT
await txnAsPromise(txn);
},
};
export default LocalIndexedDBStoreBackend;
+41 -2
View File
@@ -65,7 +65,10 @@ RemoteIndexedDBStoreBackend.prototype = {
clearDatabase: function() {
return this._ensureStarted().then(() => this._doCmd('clearDatabase'));
},
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
return this._doCmd('isNewlyCreated');
},
/**
* @return {Promise} Resolves with a sync response to restore the
* client state to where it was at the last save, or null if there
@@ -87,6 +90,40 @@ RemoteIndexedDBStoreBackend.prototype = {
return this._doCmd('syncToDatabase', [users]);
},
/**
* Returns the out-of-band membership events for this room that
* were previously loaded.
* @param {string} roomId
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
getOutOfBandMembers: function(roomId) {
return this._doCmd('getOutOfBandMembers', [roomId]);
},
/**
* Stores the out-of-band membership events for this room. Note that
* it still makes sense to store an empty array as the OOB status for the room is
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
* @param {string} roomId
* @param {event[]} membershipEvents the membership events to store
* @returns {Promise} when all members have been stored
*/
setOutOfBandMembers: function(roomId, membershipEvents) {
return this._doCmd('setOutOfBandMembers', [roomId, membershipEvents]);
},
clearOutOfBandMembers: function(roomId) {
return this._doCmd('clearOutOfBandMembers', [roomId]);
},
getClientOptions: function() {
return this._doCmd('getClientOptions');
},
storeClientOptions: function(options) {
return this._doCmd('storeClientOptions', [options]);
},
/**
* Load all user presence events from the database. This is not cached.
@@ -147,7 +184,9 @@ RemoteIndexedDBStoreBackend.prototype = {
if (msg.command == 'cmd_success') {
def.resolve(msg.result);
} else {
def.reject(msg.error);
const error = new Error(msg.error.message);
error.name = msg.error.name;
def.reject(error);
}
} else {
console.warn("Unrecognised message from worker: " + msg);
+23 -2
View File
@@ -67,6 +67,9 @@ class IndexedDBStoreWorker {
case 'connect':
prom = this.backend.connect();
break;
case 'isNewlyCreated':
prom = this.backend.isNewlyCreated();
break;
case 'clearDatabase':
prom = this.backend.clearDatabase().then((result) => {
// This returns special classes which can't be cloned
@@ -92,10 +95,25 @@ class IndexedDBStoreWorker {
case 'getNextBatchToken':
prom = this.backend.getNextBatchToken();
break;
case 'getOutOfBandMembers':
prom = this.backend.getOutOfBandMembers(msg.args[0]);
break;
case 'clearOutOfBandMembers':
prom = this.backend.clearOutOfBandMembers(msg.args[0]);
break;
case 'setOutOfBandMembers':
prom = this.backend.setOutOfBandMembers(msg.args[0], msg.args[1]);
break;
case 'getClientOptions':
prom = this.backend.getClientOptions();
break;
case 'storeClientOptions':
prom = this.backend.storeClientOptions(msg.args[0]);
break;
}
if (prom === undefined) {
postMessage({
this.postMessage({
command: 'cmd_fail',
seq: msg.seq,
// Can't be an Error because they're not structured cloneable
@@ -117,7 +135,10 @@ class IndexedDBStoreWorker {
command: 'cmd_fail',
seq: msg.seq,
// Just send a string because Error objects aren't cloneable
error: "Error running command",
error: {
message: err.message,
name: err.name,
},
});
});
}
+40
View File
@@ -146,6 +146,11 @@ IndexedDBStore.prototype.getSavedSync = function() {
return this.backend.getSavedSync();
};
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
IndexedDBStore.prototype.isNewlyCreated = function() {
return this.backend.isNewlyCreated();
};
/**
* @return {Promise} If there is a saved sync, the nextBatch token
* for this sync, otherwise null.
@@ -219,4 +224,39 @@ IndexedDBStore.prototype.setSyncData = function(syncData) {
return this.backend.setSyncData(syncData);
};
/**
* Returns the out-of-band membership events for this room that
* were previously loaded.
* @param {string} roomId
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
IndexedDBStore.prototype.getOutOfBandMembers = function(roomId) {
return this.backend.getOutOfBandMembers(roomId);
};
/**
* Stores the out-of-band membership events for this room. Note that
* it still makes sense to store an empty array as the OOB status for the room is
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
* @param {string} roomId
* @param {event[]} membershipEvents the membership events to store
* @returns {Promise} when all members have been stored
*/
IndexedDBStore.prototype.setOutOfBandMembers = function(roomId, membershipEvents) {
return this.backend.setOutOfBandMembers(roomId, membershipEvents);
};
IndexedDBStore.prototype.clearOutOfBandMembers = function(roomId) {
return this.backend.clearOutOfBandMembers(roomId);
};
IndexedDBStore.prototype.getClientOptions = function() {
return this.backend.getClientOptions();
};
IndexedDBStore.prototype.storeClientOptions = function(options) {
return this.backend.storeClientOptions(options);
};
module.exports.IndexedDBStore = IndexedDBStore;
+39
View File
@@ -52,6 +52,10 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
// type : content
};
this.localStorage = opts.localStorage;
this._oobMembers = {
// roomId: [member events]
};
this._clientOptions = {};
};
module.exports.MatrixInMemoryStore.prototype = {
@@ -64,6 +68,10 @@ module.exports.MatrixInMemoryStore.prototype = {
return this.syncToken;
},
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
return Promise.resolve(true);
},
/**
* Set the token to stream from.
@@ -377,4 +385,35 @@ module.exports.MatrixInMemoryStore.prototype = {
};
return Promise.resolve();
},
/**
* Returns the out-of-band membership events for this room that
* were previously loaded.
* @param {string} roomId
* @returns {event[]} the events, potentially an empty array if OOB loading didn't yield any new members
* @returns {null} in case the members for this room haven't been stored yet
*/
getOutOfBandMembers: function(roomId) {
return Promise.resolve(this._oobMembers[roomId] || null);
},
/**
* Stores the out-of-band membership events for this room. Note that
* it still makes sense to store an empty array as the OOB status for the room is
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null
* @param {string} roomId
* @param {event[]} membershipEvents the membership events to store
* @returns {Promise} when all members have been stored
*/
setOutOfBandMembers: function(roomId, membershipEvents) {
this._oobMembers[roomId] = membershipEvents;
return Promise.resolve();
},
getClientOptions: function() {
return Promise.resolve(this._clientOptions);
},
storeClientOptions: function(options) {
this._clientOptions = Object.assign({}, options);
return Promise.resolve();
},
};
+11
View File
@@ -190,11 +190,22 @@ WebStorageSessionStore.prototype = {
removeAllEndToEndRooms: function() {
removeByPrefix(this.store, keyEndToEndRoom(''));
},
setLocalTrustedBackupPubKey: function(pubkey) {
this.store.setItem(KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY, pubkey);
},
// XXX: This store is deprecated really, but added this as a temporary
// thing until cross-signing lands.
getLocalTrustedBackupPubKey: function() {
return this.store.getItem(KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY);
},
};
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
const KEY_END_TO_END_DEVICE_SYNC_TOKEN = E2E_PREFIX + "device_sync_token";
const KEY_END_TO_END_DEVICE_LIST_TRACKING_STATUS = E2E_PREFIX + "device_tracking";
const KEY_END_TO_END_TRUSTED_BACKUP_PUBKEY = E2E_PREFIX + "trusted_backup_pubkey";
function keyEndToEndDevicesForUser(userId) {
return E2E_PREFIX + "devices/" + userId;
+25
View File
@@ -32,6 +32,11 @@ function StubStore() {
StubStore.prototype = {
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
return Promise.resolve(true);
},
/**
* Get the sync token.
* @return {string}
@@ -264,6 +269,26 @@ StubStore.prototype = {
deleteAllData: function() {
return Promise.resolve();
},
getOutOfBandMembers: function() {
return Promise.resolve(null);
},
setOutOfBandMembers: function() {
return Promise.resolve();
},
clearOutOfBandMembers: function() {
return Promise.resolve();
},
getClientOptions: function() {
return Promise.resolve();
},
storeClientOptions: function() {
return Promise.resolve();
},
};
/** Stub Store class. */
+18 -1
View File
@@ -34,7 +34,6 @@ import utils from "./utils";
* rather than asking the server to do an initial sync on startup.
*/
class SyncAccumulator {
/**
* @param {Object} opts
* @param {Number=} opts.maxTimelineEntries The ideal maximum number of
@@ -63,6 +62,11 @@ class SyncAccumulator {
// { event: $event, token: null|token },
// ...
// ],
// _summary: {
// m.heroes: [ $user_id ],
// m.joined_member_count: $count,
// m.invited_member_count: $count
// },
// _accountData: { $event_type: json },
// _unreadNotifications: { ... unread_notifications JSON ... },
// _readReceipts: { $user_id: { data: $json, eventId: $event_id }}
@@ -242,6 +246,7 @@ class SyncAccumulator {
_timeline: [],
_accountData: Object.create(null),
_unreadNotifications: {},
_summary: {},
_readReceipts: {},
};
}
@@ -258,6 +263,17 @@ class SyncAccumulator {
if (data.unread_notifications) {
currentData._unreadNotifications = data.unread_notifications;
}
if (data.summary) {
const HEROES_KEY = "m.heroes";
const INVITED_COUNT_KEY = "m.invited_member_count";
const JOINED_COUNT_KEY = "m.joined_member_count";
const acc = currentData._summary;
const sum = data.summary;
acc[HEROES_KEY] = sum[HEROES_KEY] || acc[HEROES_KEY];
acc[JOINED_COUNT_KEY] = sum[JOINED_COUNT_KEY] || acc[JOINED_COUNT_KEY];
acc[INVITED_COUNT_KEY] = sum[INVITED_COUNT_KEY] || acc[INVITED_COUNT_KEY];
}
if (data.ephemeral && data.ephemeral.events) {
data.ephemeral.events.forEach((e) => {
@@ -428,6 +444,7 @@ class SyncAccumulator {
prev_batch: null,
},
unread_notifications: roomData._unreadNotifications,
summary: roomData._summary,
};
// Add account data
Object.keys(roomData._accountData).forEach((evType) => {
+144 -16
View File
@@ -33,6 +33,8 @@ const utils = require("./utils");
const Filter = require("./filter");
const EventTimeline = require("./models/event-timeline");
import {InvalidStoreError} from './errors';
const DEBUG = true;
// /sync requests allow you to set a timeout= but the request may continue
@@ -93,12 +95,14 @@ function SyncApi(client, opts) {
this._peekRoomId = null;
this._currentSyncRequest = null;
this._syncState = null;
this._syncStateData = null; // additional data (eg. error object for failed sync)
this._catchingUp = false;
this._running = false;
this._keepAliveTimer = null;
this._connectionReturnedDefer = null;
this._notifEvents = []; // accumulator of sync events in the current sync response
this._failedSyncCount = 0; // Number of consecutive failed /sync requests
this._storeIsInvalid = false; // flag set if the store needs to be cleared before we can start
if (client.getNotifTimelineSet()) {
client.reEmitter.reEmit(client.getNotifTimelineSet(),
@@ -112,7 +116,8 @@ function SyncApi(client, opts) {
*/
SyncApi.prototype.createRoom = function(roomId) {
const client = this.client;
const room = new Room(roomId, {
const room = new Room(roomId, client, client.getUserId(), {
lazyLoadMembers: this.opts.lazyLoadMembers,
pendingEventOrdering: this.opts.pendingEventOrdering,
timelineSupport: client.timelineSupport,
});
@@ -121,6 +126,7 @@ SyncApi.prototype.createRoom = function(roomId) {
"Room.timelineReset",
"Room.localEchoUpdated",
"Room.accountData",
"Room.myMembership",
]);
this._registerStateListeners(room);
return room;
@@ -231,7 +237,7 @@ SyncApi.prototype.syncLeftRooms = function() {
self._processRoomEvents(room, stateEvents, timelineEvents);
room.recalculate(client.credentials.userId);
room.recalculate();
client.store.storeRoom(room);
client.emit("Room", room);
@@ -302,7 +308,7 @@ SyncApi.prototype.peek = function(roomId) {
peekRoom.currentState.setStateEvents(stateEvents);
self._resolveInvites(peekRoom);
peekRoom.recalculate(self.client.credentials.userId);
peekRoom.recalculate();
// roll backwards to diverge old state. addEventsToTimeline
// will overwrite the pagination token, so make sure it overwrites
@@ -396,6 +402,18 @@ SyncApi.prototype.getSyncState = function() {
return this._syncState;
};
/**
* Returns the additional data object associated with
* the current sync state, or null if there is no
* such data.
* Sync errors, if available, are put in the 'error' key of
* this object.
* @return {?Object}
*/
SyncApi.prototype.getSyncStateData = function() {
return this._syncStateData;
};
SyncApi.prototype.recoverFromSyncStartupError = async function(savedSyncPromise, err) {
// Wait for the saved sync to complete - we send the pushrules and filter requests
// before the saved sync has finished so they can run in parallel, but only process
@@ -407,6 +425,26 @@ SyncApi.prototype.recoverFromSyncStartupError = async function(savedSyncPromise,
await keepaliveProm;
};
/**
* Is the lazy loading option different than in previous session?
* @param {bool} lazyLoadMembers current options for lazy loading
* @return {bool} whether or not the option has changed compared to the previous session */
SyncApi.prototype._wasLazyLoadingToggled = async function(lazyLoadMembers) {
lazyLoadMembers = !!lazyLoadMembers;
// assume it was turned off before
// if we don't know any better
let lazyLoadMembersBefore = false;
const isStoreNewlyCreated = await this.client.store.isNewlyCreated();
if (!isStoreNewlyCreated) {
const prevClientOptions = await this.client.store.getClientOptions();
if (prevClientOptions) {
lazyLoadMembersBefore = !!prevClientOptions.lazyLoadMembers;
}
return lazyLoadMembersBefore !== lazyLoadMembers;
}
return false;
};
/**
* Main entry point
*/
@@ -429,6 +467,8 @@ SyncApi.prototype.sync = function() {
// 1) We need to get push rules so we can check if events should bing as we get
// them from /sync.
// 2) We need to get/create a filter which we can use for /sync.
// 3) We need to check the lazy loading option matches what was used in the
// stored sync. If it doesn't, we can't use the stored sync.
async function getPushRules() {
try {
@@ -443,9 +483,47 @@ SyncApi.prototype.sync = function() {
getPushRules();
return;
}
getFilter(); // Now get the filter and start syncing
checkLazyLoadStatus(); // advance to the next stage
}
const checkLazyLoadStatus = async () => {
if (this.opts.lazyLoadMembers && client.isGuest()) {
this.opts.lazyLoadMembers = false;
}
if (this.opts.lazyLoadMembers) {
const supported = await client.doesServerSupportLazyLoading();
if (supported) {
this.opts.filter = await client.createFilter(
Filter.LAZY_LOADING_SYNC_FILTER,
);
} else {
console.log("LL: lazy loading requested but not supported " +
"by server, so disabling");
this.opts.lazyLoadMembers = false;
}
}
// need to vape the store when enabling LL and wasn't enabled before
const shouldClear = await this._wasLazyLoadingToggled(this.opts.lazyLoadMembers);
if (shouldClear) {
this._storeIsInvalid = true;
const reason = InvalidStoreError.TOGGLED_LAZY_LOADING;
const error = new InvalidStoreError(reason, !!this.opts.lazyLoadMembers);
this._updateSyncState("ERROR", { error });
// bail out of the sync loop now: the app needs to respond to this error.
// we leave the state as 'ERROR' which isn't great since this normally means
// we're retrying. The client must be stopped before clearing the stores anyway
// so the app should stop the client, clear the store and start it again.
console.warn("InvalidStoreError: store is not usable: stopping sync.");
return;
}
if (this.opts.lazyLoadMembers && this.opts.crypto) {
this.opts.crypto.enableLazyLoading();
}
await this.client._storeClientOptions();
getFilter(); // Now get the filter and start syncing
};
async function getFilter() {
let filter;
if (self.opts.filter) {
@@ -573,7 +651,12 @@ SyncApi.prototype._syncFromCache = async function(savedSync) {
console.error("Error processing cached sync", e.stack || e);
}
this._updateSyncState("PREPARED", syncEventData);
// Don't emit a prepared if we've bailed because the store is invalid:
// in this case the client will not be usable until stopped & restarted
// so this would be useless and misleading.
if (!this._storeIsInvalid) {
this._updateSyncState("PREPARED", syncEventData);
}
};
/**
@@ -763,9 +846,20 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) {
// fails, since long lived HTTP connections will
// go away sometimes and we shouldn't treat this as
// erroneous. We set the state to 'reconnecting'
// instead, so that clients can onserve this state
// instead, so that clients can observe this state
// if they wish.
this._startKeepAlives().then(() => {
this._startKeepAlives().then((connDidFail) => {
// Only emit CATCHUP if we detected a connectivity error: if we didn't,
// it's quite likely the sync will fail again for the same reason and we
// want to stay in ERROR rather than keep flip-flopping between ERROR
// and CATCHUP.
if (connDidFail && this.getSyncState() === 'ERROR') {
this._updateSyncState("CATCHUP", {
oldSyncToken: null,
nextSyncToken: null,
catchingUp: true,
});
}
this._sync(syncOptions);
});
@@ -774,6 +868,7 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) {
this._updateSyncState(
this._failedSyncCount >= FAILED_SYNC_ERROR_THRESHOLD ?
"ERROR" : "RECONNECTING",
{ error: err },
);
};
@@ -809,6 +904,11 @@ SyncApi.prototype._processSyncResponse = async function(
// state: { events: [] },
// timeline: { events: [], prev_batch: $token, limited: true },
// ephemeral: { events: [] },
// summary: {
// m.heroes: [ $user_id ],
// m.joined_member_count: $count,
// m.invited_member_count: $count
// },
// account_data: { events: [] },
// unread_notifications: {
// highlight_count: 0,
@@ -947,9 +1047,11 @@ SyncApi.prototype._processSyncResponse = async function(
const room = inviteObj.room;
const stateEvents =
self._mapSyncEventsFormat(inviteObj.invite_state, room);
room.updateMyMembership("invite");
self._processRoomEvents(room, stateEvents);
if (inviteObj.isBrandNewRoom) {
room.recalculate(client.credentials.userId);
room.recalculate();
client.store.storeRoom(room);
client.emit("Room", room);
}
@@ -976,6 +1078,8 @@ SyncApi.prototype._processSyncResponse = async function(
);
}
room.updateMyMembership("join");
joinObj.timeline = joinObj.timeline || {};
if (joinObj.isBrandNewRoom) {
@@ -1040,6 +1144,13 @@ SyncApi.prototype._processSyncResponse = async function(
self._processRoomEvents(room, stateEvents, timelineEvents);
// set summary after processing events,
// because it will trigger a name calculation
// which needs the room state to be up to date
if (joinObj.summary) {
room.setSummary(joinObj.summary);
}
// XXX: should we be adding ephemeralEvents to the timeline?
// It feels like that for symmetry with room.addAccountData()
// there should be a room.addEphemeralEvents() or similar.
@@ -1048,7 +1159,7 @@ SyncApi.prototype._processSyncResponse = async function(
// we deliberately don't add accountData to the timeline
room.addAccountData(accountDataEvents);
room.recalculate(client.credentials.userId);
room.recalculate();
if (joinObj.isBrandNewRoom) {
client.store.storeRoom(room);
client.emit("Room", room);
@@ -1061,6 +1172,16 @@ SyncApi.prototype._processSyncResponse = async function(
if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) {
await self.opts.crypto.onCryptoEvent(e);
}
if (e.isState() && e.getType() === "im.vector.user_status") {
let user = client.store.getUser(e.getStateKey());
if (user) {
user._unstable_updateStatusMessage(e);
} else {
user = createNewUser(client, e.getStateKey());
user._unstable_updateStatusMessage(e);
client.store.storeUser(user);
}
}
}
await Promise.mapSeries(stateEvents, processRoomEvent);
@@ -1083,10 +1204,12 @@ SyncApi.prototype._processSyncResponse = async function(
const accountDataEvents =
self._mapSyncEventsFormat(leaveObj.account_data);
room.updateMyMembership("leave");
self._processRoomEvents(room, stateEvents, timelineEvents);
room.addAccountData(accountDataEvents);
room.recalculate(client.credentials.userId);
room.recalculate();
if (leaveObj.isBrandNewRoom) {
client.store.storeRoom(room);
client.emit("Room", room);
@@ -1175,13 +1298,16 @@ SyncApi.prototype._startKeepAlives = function(delay) {
*
* On failure, schedules a call back to itself. On success, resolves
* this._connectionReturnedDefer.
*
* @param {bool} connDidFail True if a connectivity failure has been detected. Optional.
*/
SyncApi.prototype._pokeKeepAlive = function() {
SyncApi.prototype._pokeKeepAlive = function(connDidFail) {
if (connDidFail === undefined) connDidFail = false;
const self = this;
function success() {
clearTimeout(self._keepAliveTimer);
if (self._connectionReturnedDefer) {
self._connectionReturnedDefer.resolve();
self._connectionReturnedDefer.resolve(connDidFail);
self._connectionReturnedDefer = null;
}
}
@@ -1198,7 +1324,7 @@ SyncApi.prototype._pokeKeepAlive = function() {
).done(function() {
success();
}, function(err) {
if (err.httpStatus == 400) {
if (err.httpStatus == 400 || err.httpStatus == 404) {
// treat this as a success because the server probably just doesn't
// support /versions: point is, we're getting a response.
// We wait a short time though, just in case somehow the server
@@ -1206,8 +1332,9 @@ SyncApi.prototype._pokeKeepAlive = function() {
// responses fail, this will mean we don't hammer in a loop.
self._keepAliveTimer = setTimeout(success, 2000);
} else {
connDidFail = true;
self._keepAliveTimer = setTimeout(
self._pokeKeepAlive.bind(self),
self._pokeKeepAlive.bind(self, connDidFail),
5000 + Math.floor(Math.random() * 5000),
);
// A keepalive has failed, so we emit the
@@ -1215,7 +1342,7 @@ SyncApi.prototype._pokeKeepAlive = function() {
// first failure).
// Note we do this after setting the timer:
// this lets the unit tests advance the mock
// clock when the get the error.
// clock when they get the error.
self._updateSyncState("ERROR", { error: err });
}
});
@@ -1376,7 +1503,7 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
// a recalculation (like m.room.name) we won't recalculate until we've
// finished adding all the events, which will cause the notification to have
// the old room name rather than the new one.
room.recalculate(this.client.credentials.userId);
room.recalculate();
// If the timeline wasn't empty, we process the state events here: they're
// defined as updates to the state before the start of the timeline, so this
@@ -1451,6 +1578,7 @@ SyncApi.prototype._getGuestFilter = function() {
SyncApi.prototype._updateSyncState = function(newState, data) {
const old = this._syncState;
this._syncState = newState;
this._syncStateData = data;
this.client.emit("sync", this._syncState, old, data);
};
+48 -1
View File
@@ -19,6 +19,8 @@ limitations under the License.
* @module utils
*/
const unhomoglyph = require('unhomoglyph');
/**
* Encode a dictionary of query parameters.
* @param {Object} params A dict of key/values to encode e.g.
@@ -328,7 +330,7 @@ const deepCompare = module.exports.deepCompare = function(x, y) {
}
// finally, compare each of x's keys with y
for (p in y) {
for (p in y) { // eslint-disable-line guard-for-in
if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
return false;
}
@@ -652,3 +654,48 @@ module.exports.inherits = function(ctor, superCtor) {
},
});
};
/**
* Returns whether the given value is a finite number without type-coercion
*
* @param {*} value the value to test
* @return {boolean} whether or not value is a finite number without type-coercion
*/
module.exports.isNumber = function(value) {
return typeof value === 'number' && isFinite(value);
};
/**
* Removes zero width chars, diacritics and whitespace from the string
* Also applies an unhomoglyph on the string, to prevent similar looking chars
* @param {string} str the string to remove hidden characters from
* @return {string} a string with the hidden characters removed
*/
module.exports.removeHiddenChars = function(str) {
return unhomoglyph(str.normalize('NFD').replace(removeHiddenCharsRegex, ''));
};
const removeHiddenCharsRegex = /[\u200B-\u200D\u0300-\u036f\uFEFF\s]/g;
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
module.exports.escapeRegExp = escapeRegExp;
module.exports.globToRegexp = function(glob, extended) {
extended = typeof(extended) === 'boolean' ? extended : true;
// 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(/\\\*/g, '.*');
pat = pat.replace(/\?/g, '.');
if (extended) {
pat = pat.replace(/\\\[(!|)(.*)\\]/g, function(match, p1, p2, offset, string) {
const first = p1 && '^' || '';
const second = p2.replace(/\\-/, '-');
return '[' + first + second + ']';
});
}
return pat;
};
+18 -6
View File
@@ -583,7 +583,7 @@ MatrixCall.prototype._maybeGotUserMediaForInvite = function(stream) {
' Or possibly you are using an insecure domain. Receiving only.');
this.peerConn = _createPeerConnection(this);
} else {
debuglog('Failed to getUserMedia.');
debuglog('Failed to getUserMedia: ' + error.name);
this._getUserMediaFailed(error);
return;
}
@@ -652,7 +652,7 @@ MatrixCall.prototype._maybeGotUserMediaForAnswer = function(stream) {
debuglog('User denied access to camera/microphone.' +
' Or possibly you are using an insecure domain. Receiving only.');
} else {
debuglog('Failed to getUserMedia.');
debuglog('Failed to getUserMedia: ' + error.name);
this._getUserMediaFailed(error);
return;
}
@@ -1121,9 +1121,13 @@ const _tryPlayRemoteStream = function(self) {
}
};
const _tryPlayRemoteAudioStream = function(self) {
const _tryPlayRemoteAudioStream = async function(self) {
if (self.getRemoteAudioElement() && self.remoteAStream) {
const player = self.getRemoteAudioElement();
// if audioOutput is non-default:
if (audioOutput) await player.setSinkId(audioOutput);
player.autoplay = true;
self.assignElement(player, self.remoteAStream, "remoteAudio");
setTimeout(function() {
@@ -1270,15 +1274,15 @@ const _getUserMediaVideoContraints = function(callType) {
case 'voice':
return {
audio: {
deviceId: audioInput ? {exact: audioInput} : undefined,
deviceId: audioInput ? {ideal: audioInput} : undefined,
}, video: false,
};
case 'video':
return {
audio: {
deviceId: audioInput ? {exact: audioInput} : undefined,
deviceId: audioInput ? {ideal: audioInput} : undefined,
}, video: {
deviceId: videoInput ? {exact: videoInput} : undefined,
deviceId: videoInput ? {ideal: videoInput} : undefined,
/* We want 640x360. Chrome will give it only if we ask exactly,
FF refuses entirely if we ask exactly, so have to ask for ideal
instead */
@@ -1317,8 +1321,16 @@ const forAllTracksOnStream = function(s, f) {
/** The MatrixCall class. */
module.exports.MatrixCall = MatrixCall;
let audioOutput;
let audioInput;
let videoInput;
/**
* Set an audio output device to use for MatrixCalls
* @function
* @param {string=} deviceId the identifier for the device
* undefined treated as unset
*/
module.exports.setAudioOutput = function(deviceId) { audioOutput = deviceId; };
/**
* Set an audio input device to use for MatrixCalls
* @function
+3 -7
View File
@@ -2,12 +2,8 @@
set -ex
npm run lint
yarn lint
# install Olm so that we can run the crypto tests.
npm install https://matrix.org/packages/npm/olm/olm-2.2.2.tgz
npm run test
npm run gendoc
yarn test
yarn gendoc
+4882
View File
File diff suppressed because it is too large Load Diff