From 9a53fa3876e5ec86321f0c405e9f83090a998d99 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Sun, 31 Dec 2017 01:24:27 +0100 Subject: [PATCH 001/472] Fix typo around getThirdpartyLocation --- src/base-apis.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base-apis.js b/src/base-apis.js index 3ae681917..2b8578d2d 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -1668,7 +1668,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 */ From 81de2b3afc47b7157eed358d92805c1453474148 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Sun, 31 Dec 2017 01:24:55 +0100 Subject: [PATCH 002/472] Add getThirdpartyUser Signed-off-by: Johannes Bornhold --- src/base-apis.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/base-apis.js b/src/base-apis.js index 2b8578d2d..b8f6a156f 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -1683,6 +1683,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 */ From fb1b554b862677848e94dc0e1cf74e6ea184419a Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 15 Jan 2018 01:50:24 +0000 Subject: [PATCH 003/472] initial pseudocode WIP for e2e online backups --- src/client.js | 1 + src/crypto/OlmDevice.js | 20 ++++++++++++++++++++ src/crypto/index.js | 14 ++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/src/client.js b/src/client.js index 0b00ab36b..293571185 100644 --- a/src/client.js +++ b/src/client.js @@ -393,6 +393,7 @@ MatrixClient.prototype.initCrypto = async function() { "crypto.roomKeyRequest", "crypto.roomKeyRequestCancellation", "crypto.warning", + "crypto.suggestKeyRestore", ]); await crypto.init(); diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index cda14779c..131419ea7 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -91,6 +91,21 @@ function OlmDevice(sessionStore, cryptoStore) { this.deviceEd25519Key = null; this._maxOneTimeKeys = null; + // track whether this device's megolm keys are being backed up incrementally + // to the server or not. + // XXX: this should probably have a single source of truth from OlmAccount + this.backupKey = null; + + // track which of our other devices (if any) have cross-signed this device + // XXX: this should probably have a single source of truth in the /devices + // API store or whatever we use to track our self-signed devices. + this.crossSelfSigs = []; + + // track whether we have already suggested to the user that they should + // restore their keys from backup or by cross-signing the device. + // We use this to avoid repeatedly emitting the suggestion event. + this.suggestedKeyRestore = false; + // we don't bother stashing outboundgroupsessions in the sessionstore - // instead we keep them here. this._outboundGroupSessionStore = {}; @@ -921,6 +936,11 @@ OlmDevice.prototype.addInboundGroupSession = async function( this._cryptoStore.addEndToEndInboundGroupSession( senderKey, sessionId, sessionData, txn, ); + + if (this.backupKey) { + // get olm::Account::generate_backup_encryption_secret + // save sessionData (pickled with this secret) to the server + } } finally { session.free(); } diff --git a/src/crypto/index.js b/src/crypto/index.js index 6b1d8f477..31dcd76b6 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1015,6 +1015,13 @@ Crypto.prototype._onRoomKeyEvent = function(event) { return; } + if (!device.suggestedKeyRestore && + !device.backupKey && !device.selfCrossSigs.length) + { + this.emit("crypto.suggestKeyRestore"); + device.suggestKeyRestore = true; + } + const alg = this._getRoomDecryptor(content.room_id, content.algorithm); alg.onRoomKeyEvent(event); }; @@ -1355,6 +1362,13 @@ class IncomingRoomKeyRequestCancellation { * @param {module:crypto~IncomingRoomKeyRequestCancellation} req */ +/** + * Fires when we want to suggest to the user that they restore their megolm keys + * from backup or by cross-signing the device. + * + * @event module:client~MatrixClient#"crypto.suggestKeyRestore" + */ + /** * Fires when the app may wish to warn the user about something related * the end-to-end crypto. From e0c9b990e7db19bed24129b9307b37ed4600c765 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 18 Jan 2018 20:59:08 +0000 Subject: [PATCH 004/472] blindly move crypto.suggestKeyRestore over to /sync --- src/client.js | 7 ++++++- src/crypto/OlmDevice.js | 1 + src/crypto/index.js | 14 -------------- src/sync.js | 10 ++++++++++ 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/client.js b/src/client.js index 293571185..aa05462ac 100644 --- a/src/client.js +++ b/src/client.js @@ -393,7 +393,6 @@ MatrixClient.prototype.initCrypto = async function() { "crypto.roomKeyRequest", "crypto.roomKeyRequestCancellation", "crypto.warning", - "crypto.suggestKeyRestore", ]); await crypto.init(); @@ -3630,6 +3629,12 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * }); */ +/** + * Fires when we want to suggest to the user that they restore their megolm keys + * from backup or by cross-signing the device. + * + * @event module:client~MatrixClient#"crypto.suggestKeyRestore" + */ // EventEmitter JSDocs diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 131419ea7..656e4fddc 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -104,6 +104,7 @@ function OlmDevice(sessionStore, cryptoStore) { // track whether we have already suggested to the user that they should // restore their keys from backup or by cross-signing the device. // We use this to avoid repeatedly emitting the suggestion event. + // XXX: persist this somewhere! this.suggestedKeyRestore = false; // we don't bother stashing outboundgroupsessions in the sessionstore - diff --git a/src/crypto/index.js b/src/crypto/index.js index 31dcd76b6..6b1d8f477 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1015,13 +1015,6 @@ Crypto.prototype._onRoomKeyEvent = function(event) { return; } - if (!device.suggestedKeyRestore && - !device.backupKey && !device.selfCrossSigs.length) - { - this.emit("crypto.suggestKeyRestore"); - device.suggestKeyRestore = true; - } - const alg = this._getRoomDecryptor(content.room_id, content.algorithm); alg.onRoomKeyEvent(event); }; @@ -1362,13 +1355,6 @@ class IncomingRoomKeyRequestCancellation { * @param {module:crypto~IncomingRoomKeyRequestCancellation} req */ -/** - * Fires when we want to suggest to the user that they restore their megolm keys - * from backup or by cross-signing the device. - * - * @event module:client~MatrixClient#"crypto.suggestKeyRestore" - */ - /** * Fires when the app may wish to warn the user about something related * the end-to-end crypto. diff --git a/src/sync.js b/src/sync.js index 71fb866d5..74aff54bf 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations 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. @@ -983,6 +984,15 @@ SyncApi.prototype._processSyncResponse = async function( async function processRoomEvent(e) { client.emit("event", e); if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) { + + // XXX: get device + if (!device.getSuggestedKeyRestore() && + !device.backupKey && !device.selfCrossSigs.length) + { + client.emit("crypto.suggestKeyRestore"); + device.setSuggestedKeyRestore(true); + } + await self.opts.crypto.onCryptoEvent(e); } } From 6570402b95d015a5b2f8e2b362d317c93e145f81 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 3 May 2018 13:42:33 +0100 Subject: [PATCH 005/472] Add getMediaLimits to client --- src/client.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/client.js b/src/client.js index 892e144f1..4af13ba44 100644 --- a/src/client.js +++ b/src/client.js @@ -727,6 +727,20 @@ MatrixClient.prototype.getGroups = function() { return this.store.getGroups(); }; +/** + * Get the room for the given room ID. + * This function will return a valid room for any room for which a Room event + * has been emitted. Note in particular that other events, eg. RoomState.members + * will be emitted for a room before this function will return the given room. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves with an object containing the limits. + */ +MatrixClient.prototype.getMediaLimits = function(callback) { + return return this._http.requestWithPrefix( + callback, "GET", "/limits", undefined, undefined, httpApi.PREFIX_MEDIA_R0, + ) +}; + // Room ops // ======== From 9596087959ee941c53f2f36c8fdca495ce39d602 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 3 May 2018 17:31:04 +0100 Subject: [PATCH 006/472] Remove extra return --- src/client.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.js b/src/client.js index 4af13ba44..5ca65d5b5 100644 --- a/src/client.js +++ b/src/client.js @@ -736,9 +736,9 @@ MatrixClient.prototype.getGroups = function() { * @return {module:client.Promise} Resolves with an object containing the limits. */ MatrixClient.prototype.getMediaLimits = function(callback) { - return return this._http.requestWithPrefix( + return this._http.requestWithPrefix( callback, "GET", "/limits", undefined, undefined, httpApi.PREFIX_MEDIA_R0, - ) + ); }; // Room ops From 9cdcbf6bf8ce6f7e1327aa565875b17d57b0a52e Mon Sep 17 00:00:00 2001 From: Leon Date: Tue, 22 May 2018 11:27:05 +0800 Subject: [PATCH 007/472] emit oldEventId on "updatePendingEvent" It should fire `Room.localEchoUpdated` event and returns the oldEventId --- src/models/room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room.js b/src/models/room.js index 110b08c39..20a2e95b9 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -817,7 +817,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); }; From 8dd425f8ff0b67ce0dbca326d2a66eac3637be2d Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Wed, 20 Jun 2018 17:24:45 +0100 Subject: [PATCH 008/472] Media/limits => /config --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 5ca65d5b5..25e8314f3 100644 --- a/src/client.js +++ b/src/client.js @@ -737,7 +737,7 @@ MatrixClient.prototype.getGroups = function() { */ MatrixClient.prototype.getMediaLimits = function(callback) { return this._http.requestWithPrefix( - callback, "GET", "/limits", undefined, undefined, httpApi.PREFIX_MEDIA_R0, + callback, "GET", "/config", undefined, undefined, httpApi.PREFIX_MEDIA_R0, ); }; From fde00b1c627e174ee7dbb76efc06bec1a46f52d6 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Sat, 23 Jun 2018 12:57:29 +0100 Subject: [PATCH 009/472] getMediaLimits -> getMediaConfig --- src/client.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/client.js b/src/client.js index 25e8314f3..a684c5e01 100644 --- a/src/client.js +++ b/src/client.js @@ -728,14 +728,11 @@ MatrixClient.prototype.getGroups = function() { }; /** - * Get the room for the given room ID. - * This function will return a valid room for any room for which a Room event - * has been emitted. Note in particular that other events, eg. RoomState.members - * will be emitted for a room before this function will return the given room. + * Get the config for the media repository. * @param {module:client.callback} callback Optional. - * @return {module:client.Promise} Resolves with an object containing the limits. + * @return {module:client.Promise} Resolves with an object containing the config. */ -MatrixClient.prototype.getMediaLimits = function(callback) { +MatrixClient.prototype.getMediaConfig = function(callback) { return this._http.requestWithPrefix( callback, "GET", "/config", undefined, undefined, httpApi.PREFIX_MEDIA_R0, ); From a54f30c02fad9fa04adc3ea063be877fdb5581d6 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 3 May 2018 13:42:33 +0100 Subject: [PATCH 010/472] Add getMediaLimits to client --- src/client.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/client.js b/src/client.js index e8c44c653..1e7a12781 100644 --- a/src/client.js +++ b/src/client.js @@ -727,6 +727,20 @@ MatrixClient.prototype.getGroups = function() { return this.store.getGroups(); }; +/** + * Get the room for the given room ID. + * This function will return a valid room for any room for which a Room event + * has been emitted. Note in particular that other events, eg. RoomState.members + * will be emitted for a room before this function will return the given room. + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves with an object containing the limits. + */ +MatrixClient.prototype.getMediaLimits = function(callback) { + return return this._http.requestWithPrefix( + callback, "GET", "/limits", undefined, undefined, httpApi.PREFIX_MEDIA_R0, + ) +}; + // Room ops // ======== From 68c23af5aea1ce823b51f7c5f7f5a6e61dc5bfcb Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 3 May 2018 17:31:04 +0100 Subject: [PATCH 011/472] Remove extra return --- src/client.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.js b/src/client.js index 1e7a12781..8f1d921be 100644 --- a/src/client.js +++ b/src/client.js @@ -736,9 +736,9 @@ MatrixClient.prototype.getGroups = function() { * @return {module:client.Promise} Resolves with an object containing the limits. */ MatrixClient.prototype.getMediaLimits = function(callback) { - return return this._http.requestWithPrefix( + return this._http.requestWithPrefix( callback, "GET", "/limits", undefined, undefined, httpApi.PREFIX_MEDIA_R0, - ) + ); }; // Room ops From 329f09ce0a734ab3d5abbf975a7012de127987af Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Wed, 20 Jun 2018 17:24:45 +0100 Subject: [PATCH 012/472] Media/limits => /config --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 8f1d921be..7ab2203d0 100644 --- a/src/client.js +++ b/src/client.js @@ -737,7 +737,7 @@ MatrixClient.prototype.getGroups = function() { */ MatrixClient.prototype.getMediaLimits = function(callback) { return this._http.requestWithPrefix( - callback, "GET", "/limits", undefined, undefined, httpApi.PREFIX_MEDIA_R0, + callback, "GET", "/config", undefined, undefined, httpApi.PREFIX_MEDIA_R0, ); }; From aec7ef6f9c8166bc087c8d4d1f8a7013aa6d5c12 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Sat, 23 Jun 2018 12:57:29 +0100 Subject: [PATCH 013/472] getMediaLimits -> getMediaConfig --- src/client.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/client.js b/src/client.js index 7ab2203d0..899a27d06 100644 --- a/src/client.js +++ b/src/client.js @@ -728,14 +728,11 @@ MatrixClient.prototype.getGroups = function() { }; /** - * Get the room for the given room ID. - * This function will return a valid room for any room for which a Room event - * has been emitted. Note in particular that other events, eg. RoomState.members - * will be emitted for a room before this function will return the given room. + * Get the config for the media repository. * @param {module:client.callback} callback Optional. - * @return {module:client.Promise} Resolves with an object containing the limits. + * @return {module:client.Promise} Resolves with an object containing the config. */ -MatrixClient.prototype.getMediaLimits = function(callback) { +MatrixClient.prototype.getMediaConfig = function(callback) { return this._http.requestWithPrefix( callback, "GET", "/config", undefined, undefined, httpApi.PREFIX_MEDIA_R0, ); From bce2ba07853b561c7557354dfc86766f121d9ac2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Jul 2018 18:15:13 +0100 Subject: [PATCH 014/472] Prepare changelog for v0.10.7-rc.1 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d96c82612..fa279da11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +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) From c429ca67b95730816818376ddfc1dc46f4385fe0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Jul 2018 18:15:14 +0100 Subject: [PATCH 015/472] v0.10.7-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2796f502d..3cafc8f7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.10.6", + "version": "0.10.7-rc.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From 94bbba72f5d908c57a426350acba4c95a7d4af38 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 11:35:19 +0200 Subject: [PATCH 016/472] add member to members before emitting any events --- src/models/room-state.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/models/room-state.js b/src/models/room-state.js index 1ca3388c3..35cf0dbbd 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -217,8 +217,13 @@ RoomState.prototype.setStateEvents = function(stateEvents) { } let member = self.members[userId]; + self._joinedMemberCount = null; + if (!member) { member = new RoomMember(event.getRoomId(), userId); + // add member to members before emitting any events, + // as event handlers often lookup the member + self.members[userId] = member; self.emit("RoomState.newMember", event, self, member); } @@ -232,8 +237,6 @@ RoomState.prototype.setStateEvents = function(stateEvents) { // blow away the sentinel which is now outdated delete self._sentinels[userId]; - self.members[userId] = member; - self._joinedMemberCount = null; self.emit("RoomState.members", event, self, member); } else if (event.getType() === "m.room.power_levels") { const members = utils.values(self.members); From e3e48944e0b13fab6181c6c520eff7bcabb624fb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 27 Jul 2018 11:49:55 +0200 Subject: [PATCH 017/472] add test --- spec/unit/room-state.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/unit/room-state.spec.js b/spec/unit/room-state.spec.js index 21eafba4a..66e7df1be 100644 --- a/spec/unit/room-state.spec.js +++ b/spec/unit/room-state.spec.js @@ -162,6 +162,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; From 35b7f358b6c2b2f9f534da84226ce2c80f1059d8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 30 Jul 2018 11:40:08 +0100 Subject: [PATCH 018/472] Prepare changelog for v0.10.7 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa279da11..f4bf276ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +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) From 0e81dfb00479cae030b8c70c718ec89d011f75c6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 30 Jul 2018 11:40:08 +0100 Subject: [PATCH 019/472] v0.10.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3cafc8f7f..15eac9216 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.10.7-rc.1", + "version": "0.10.7", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From 3012501e4b731f847dd2f1dd7a6ba2af2fc1ae51 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 30 Jul 2018 14:19:45 +0200 Subject: [PATCH 020/472] update docs to clarify state when emitting newMember --- src/models/room-state.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/models/room-state.js b/src/models/room-state.js index 35cf0dbbd..9e9b6b84c 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -546,7 +546,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 From 630dfa94995f83b0b1ef1b1d0ea92b6569b0b72c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Jul 2018 15:33:56 +0200 Subject: [PATCH 021/472] make sure room.tags is always a valid object so no crashes happen later on --- src/models/room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room.js b/src/models/room.js index b426c7490..4f7ac3911 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1143,7 +1143,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? From c2f3324302716d9769d4db181f5609af20a53f3b Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Aug 2018 16:59:10 +0100 Subject: [PATCH 022/472] Send sync error to listener We do this in other places, but not here --- src/sync.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sync.js b/src/sync.js index 2636460b9..6d56a4fdc 100644 --- a/src/sync.js +++ b/src/sync.js @@ -763,7 +763,7 @@ 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._sync(syncOptions); @@ -774,6 +774,7 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) { this._updateSyncState( this._failedSyncCount >= FAILED_SYNC_ERROR_THRESHOLD ? "ERROR" : "RECONNECTING", + { error: err }, ); }; From 89ad104423524d5ba55a1767855aa447d7dfab0d Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Aug 2018 18:00:52 +0100 Subject: [PATCH 023/472] Add getSyncStateData() To get additional information about the sync state (ie. the error object). --- src/client.js | 15 +++++++++++++++ src/sync.js | 14 ++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/client.js b/src/client.js index e8c44c653..ab8c79b5f 100644 --- a/src/client.js +++ b/src/client.js @@ -287,6 +287,21 @@ MatrixClient.prototype.getSyncState = function() { return this._syncApi.getSyncState(); }; +/** + * 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} + */ +MatrixClient.prototype.getSyncStateData = function() { + if (!this._syncApi) { + return null; + } + return this._syncApi.getSyncStateData(); +}; + /** * Return whether the client is configured for a guest account. * @return {boolean} True if this is a guest access_token (or no token is supplied). diff --git a/src/sync.js b/src/sync.js index 2636460b9..74e000485 100644 --- a/src/sync.js +++ b/src/sync.js @@ -93,6 +93,7 @@ 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; @@ -396,6 +397,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 @@ -1451,6 +1464,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); }; From d55618921bb584164417297162ab22ba0df08564 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Tue, 7 Aug 2018 23:10:55 -0400 Subject: [PATCH 024/472] initial implementation of e2e key backup and restore --- spec/unit/crypto/backup.spec.js | 218 ++++++++++++++++++++++++++++++++ src/client.js | 84 ++++++++++++ src/crypto/OlmDevice.js | 10 -- src/crypto/algorithms/megolm.js | 56 +++++++- src/crypto/index.js | 5 + src/sync.js | 4 +- 6 files changed, 365 insertions(+), 12 deletions(-) create mode 100644 spec/unit/crypto/backup.spec.js diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js new file mode 100644 index 000000000..b6ec6dce5 --- /dev/null +++ b/spec/unit/crypto/backup.spec.js @@ -0,0 +1,218 @@ +try { + global.Olm = require('olm'); +} catch (e) { + console.warn("unable to run megolm backup tests: libolm not available"); +} + +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'; + +// 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'); +} + +const MatrixClient = sdk.MatrixClient; +const MatrixEvent = sdk.MatrixEvent; +const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; + +const ROOM_ID = '!ROOM:ID'; + +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(function () { + 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/Og0mOBr066SpjqqbTmoK"); + + 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; + + beforeEach(function() { + mockBaseApis = {}; + + megolmDecryption = new MegolmDecryption({ + userId: '@user:id', + crypto: mockCrypto, + olmDevice: olmDevice, + baseApis: mockBaseApis, + roomId: ROOM_ID, + }); + + megolmDecryption.olmlib = mockOlmLib; + }); + + it('automatically backs up keys', function() { + const groupSession = new global.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); + }; + + const sessionId = groupSession.session_id(); + const cipherText = groupSession.encrypt(JSON.stringify({ + room_id: ROOM_ID, + content: 'testytest', + })); + const msgevent = new MatrixEvent({ + type: 'm.room.encrypted', + room_id: ROOM_ID, + content: { + algorithm: 'm.megolm.v1.aes-sha2', + sender_key: "SENDER_CURVE25519", + session_id: sessionId, + ciphertext: cipherText, + }, + event_id: "$event1", + origin_server_ts: 1507753886000, + }); + + mockBaseApis.sendKeyBackup = expect.createSpy(); + + return event.attemptDecryption(mockCrypto).then(() => { + return megolmDecryption.onRoomKeyEvent(event); + }).then(() => { + expect(mockBaseApis.sendKeyBackup).toHaveBeenCalled(); + }); + }); + }); + + describe("restore", function () { + let client; + + beforeEach(function() { + 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)); + 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(); + }); + + it('can restore from backup', function () { + const event = new MatrixEvent({ + type: 'm.room.encrypted', + room_id: '!ROOM:ID', + content: { + algorithm: 'm.megolm.v1.aes-sha2', + sender_key: 'SENDER_CURVE25519', + session_id: 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', + ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/NCiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBlmkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs' + }, + event_id: '$event1', + origin_server_ts: 1507753886000, + }); + client._http.authedRequest = function () { + return Promise.resolve({ + data: { + first_message_index: 0, + forwarded_count: 0, + is_verified: false, + session_data: { + ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZSlne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOySyw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGFru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxvC+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpeUg5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3NfQHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPyiie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg', + mac: '5lxYBHQU80M', + ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14', + } + }, + headers: {}, + code: 200 + }); + }; + const decryption = new Olm.PkDecryption(); + decryption.unpickle("secret_key", "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZDQWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA"); + return client.restoreKeyBackups(decryption, ROOM_ID, 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc') + .then(() => { + return megolmDecryption.decryptEvent(event); + }).then((res) => { + expect(res.clearEvent.content).toEqual('testytest'); + }); + }); + }); +}); diff --git a/src/client.js b/src/client.js index 2a0002df5..7aa40d09d 100644 --- a/src/client.js +++ b/src/client.js @@ -703,6 +703,90 @@ MatrixClient.prototype.importRoomKeys = function(keys) { return this._crypto.importRoomKeys(keys); }; +MatrixClient.prototype._makeKeyBackupPath = function(roomId, sessionId, version) { + let path; + if (sessionId !== undefined) { + path = utils.encodeUri("/room_keys/keys/$roomId/$sessionId", { + $roomId: roomId, + $sessionId: sessionId, + }); + } else if (roomId !== undefined) { + path = utils.encodeUri("/room_keys/keys/$roomId", { + $roomId: roomId, + }); + } else { + path = "/room_keys/keys"; + } + const queryData = version === undefined ? undefined : {version : version}; + return { + path: path, + queryData: queryData, + } +} + +/** + * Back up session keys to the homeserver. + * @param {string} roomId ID of the room that the keys are for Optional. + * @param {string} sessionId ID of the session that the keys are for Optional. + * @param {integer} version backup version Optional. + * @param {object} key data + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} a promise that will resolve when the keys + * are uploaded + */ +MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data, callback) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const path = this._makeKeyBackupPath(roomId, sessionId, version); + return this._http.authedRequest( + callback, "PUT", path.path, path.queryData, data, + ); +}; + +MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, roomId, sessionId, version, callback) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const path = this._makeKeyBackupPath(roomId, sessionId, version); + return this._http.authedRequest( + undefined, "GET", path.path, path.queryData, + ).then((response) => { + if (response.code === 200) { + const keys = []; + // FIXME: for each room, session, if response has multiple + // decrypt response.data.session_data + const data = response.data; + const key = JSON.parse(decryptionKey.decrypt(data.session_data.ephemeral, data.session_data.mac, data.session_data.ciphertext)); + // set room_id and session_id + key.room_id = roomId; + key.session_id = sessionId; + keys.push(key); + return this.importRoomKeys(keys); + } else { + callback("aargh!"); + return Promise.reject("aaargh!"); + } + }).then(() => { + if (callback) { + callback(); + } + }) +}; + +MatrixClient.prototype.deleteKeyBackups = function(roomId, sessionId, version, callback) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const path = this._makeKeyBackupPath(roomId, sessionId, version); + return this._http.authedRequest( + callback, "DELETE", path.path, path.queryData, + ) +}; + // Group ops // ========= // Operations on groups that come down the sync stream (ie. ones the diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 656e4fddc..950faa8e2 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -91,11 +91,6 @@ function OlmDevice(sessionStore, cryptoStore) { this.deviceEd25519Key = null; this._maxOneTimeKeys = null; - // track whether this device's megolm keys are being backed up incrementally - // to the server or not. - // XXX: this should probably have a single source of truth from OlmAccount - this.backupKey = null; - // track which of our other devices (if any) have cross-signed this device // XXX: this should probably have a single source of truth in the /devices // API store or whatever we use to track our self-signed devices. @@ -937,11 +932,6 @@ OlmDevice.prototype.addInboundGroupSession = async function( this._cryptoStore.addEndToEndInboundGroupSession( senderKey, sessionId, sessionData, txn, ); - - if (this.backupKey) { - // get olm::Account::generate_backup_encryption_secret - // save sessionData (pickled with this secret) to the server - } } finally { session.free(); } diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index 005d157c5..b246fc141 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -804,7 +804,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { } console.log(`Adding key for megolm session ${senderKey}|${sessionId}`); - this._olmDevice.addInboundGroupSession( + return this._olmDevice.addInboundGroupSession( content.room_id, senderKey, forwardingKeyChain, sessionId, content.session_key, keysClaimed, exportFormat, @@ -819,6 +819,12 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { // have another go at decrypting events sent with this session. this._retryDecryption(senderKey, sessionId); + }).then(() => { + return this.backupGroupSession( + content.room_id, senderKey, forwardingKeyChain, + content.session_id, content.session_key, keysClaimed, + exportFormat, + ); }).catch((e) => { console.error(`Error handling m.room_key_event: ${e}`); }); @@ -941,6 +947,54 @@ MegolmDecryption.prototype.importRoomKey = function(session) { }); }; +MegolmDecryption.prototype.backupGroupSession = async function( + roomId, senderKey, forwardingCurve25519KeyChain, + sessionId, sessionKey, keysClaimed, + exportFormat, +) { + // new session. + const session = new Olm.InboundGroupSession(); + let first_known_index; + try { + if (exportFormat) { + session.import_session(sessionKey); + } else { + session.create(sessionKey); + } + if (sessionId != session.session_id()) { + throw new Error( + "Mismatched group session ID from senderKey: " + + senderKey, + ); + } + + if (!exportFormat) { + sessionKey = session.export_session(); + } + const first_known_index = session.first_known_index(); + + const sessionData = { + algorithm: olmlib.MEGOLM_ALGORITHM, + sender_key: senderKey, + sender_claimed_keys: keysClaimed, + forwardingCurve25519KeyChain: forwardingCurve25519KeyChain, + session_key: sessionKey + }; + const encrypted = this._crypto.backupKey.encrypt(JSON.stringify(sessionData)); + const data = { + first_message_index: first_known_index, + forwarded_count: forwardingCurve25519KeyChain.length, + is_verified: false, // FIXME: how do we determine this? + session_data: encrypted + }; + return this._baseApis.sendKeyBackup(roomId, sessionId, data); + } catch (e) { + return Promise.reject(e); + } finally { + session.free(); + } +} + /** * Have another go at decrypting events after we receive a key * diff --git a/src/crypto/index.js b/src/crypto/index.js index 703bd0631..fe233f2cf 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -72,6 +72,11 @@ function Crypto(baseApis, sessionStore, userId, deviceId, this._cryptoStore = cryptoStore; this._roomList = roomList; + // track whether this device's megolm keys are being backed up incrementally + // to the server or not. + // XXX: this should probably have a single source of truth from OlmAccount + this.backupKey = null; + this._olmDevice = new OlmDevice(sessionStore, cryptoStore); this._deviceList = new DeviceList( baseApis, cryptoStore, sessionStore, this._olmDevice, diff --git a/src/sync.js b/src/sync.js index b85f838f6..7c769f20e 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1060,13 +1060,15 @@ SyncApi.prototype._processSyncResponse = async function( client.emit("event", e); if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) { + /* // XXX: get device - if (!device.getSuggestedKeyRestore() && + if (!device.getSuggestedKeyRestore() && !device.backupKey && !device.selfCrossSigs.length) { client.emit("crypto.suggestKeyRestore"); device.setSuggestedKeyRestore(true); } + */ await self.opts.crypto.onCryptoEvent(e); } From f72f5b43e1e0293dca19fc48255e0df3a9a7fde4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 14 Aug 2018 14:27:08 +0100 Subject: [PATCH 025/472] Add getVersion to Room To get the version number of the room as per https://github.com/matrix-org/matrix-doc/issues/1425 --- src/models/room.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/models/room.js b/src/models/room.js index 4f7ac3911..7e01eab64 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -174,6 +174,18 @@ function Room(roomId, opts) { } 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) return null; + const ver = createEvent.getContent()['room_version']; + if (ver === undefined) return '1'; + return ver; +}; + /** * Get the list of pending sent events for this room * From f012ada2c4b6433e1069bf46cbafbe39d8e44692 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Jul 2018 17:15:34 +0200 Subject: [PATCH 026/472] add setter on RoomMember to update from lazily loaded member --- src/models/room-member.js | 40 ++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/models/room-member.js b/src/models/room-member.js index 02fd5b344..b52b6014b 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -58,6 +58,7 @@ function RoomMember(roomId, userId) { this.events = { member: null, }; + this._lazyLoadAvatarUrl = null; this._updateModifiedTime(); } utils.inherits(RoomMember, EventEmitter); @@ -81,7 +82,11 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) { 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(); @@ -92,6 +97,16 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) { this.emit("RoomMember.name", event, this, oldName); } }; +/** + * Update this room member from a lazily loaded member + */ +RoomMember.prototype.setAsJoinedMember = function(displayName, avatarUrl, roomState) { + this.membership = "join"; + this.name = calculateDisplayName(this.userId, displayName, roomState); + this.rawDisplayName = displayName || this.userId; + this._lazyLoadAvatarUrl = avatarUrl; + //TODO: race condition between existing membership events since started syncing +} /** * Update this room member's power level event. May fire @@ -200,10 +215,12 @@ RoomMember.prototype.getAvatarUrl = if (allowDefault === undefined) { allowDefault = true; } - if (!this.events.member && !allowDefault) { + + const rawUrl = this._getRawAvatarMxcUrl(); + + 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,11 +233,20 @@ 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._getRawAvatarMxcUrl = function() { + if (this._lazyLoadAvatarUrl) { + return this._lazyLoadAvatarUrl; + } else if(this.events.member) { + return this.events.member.getContent().avatar_url; + } + return null; +} -function calculateDisplayName(member, event, roomState) { - const displayName = event.getDirectionalContent().displayname; - const selfUserId = member.userId; - +function calculateDisplayName(selfUserId, displayName, roomState) { if (!displayName) { return selfUserId; } From 2b5925b89326856457bedb11804847ff37e5a1f8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Jul 2018 17:16:27 +0200 Subject: [PATCH 027/472] Support for updating members in RoomState from lazily loaded members --- src/models/room-state.js | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/models/room-state.js b/src/models/room-state.js index 9e9b6b84c..d557f4e03 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -228,15 +228,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) { } 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._updateMember(member); self.emit("RoomState.members", event, self, member); } else if (event.getType() === "m.room.power_levels") { const members = utils.values(self.members); @@ -251,6 +243,35 @@ RoomState.prototype.setStateEvents = function(stateEvents) { }); }; +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; +} + +RoomState.prototype.setJoinedMembers = function(joinedMembers) { + const joinedRoomMembers = Object.entries(joinedMembers).map(([userId, details]) => { + const displayName = details.display_name; + const avatarUrl = details.avatar_url; + const member = new RoomMember(this.roomId, userId); + member.setAsJoinedMember(displayName, avatarUrl, this); + return member; + }); + joinedRoomMembers.forEach(member => { + _updateDisplayNameCache(this, member.userId, member.name); + this._updateMember(member); + }); + this.emit("Room"); +} + /** * Set the current typing event for this room. * @param {MatrixEvent} event The typing event From 2c5cad71ee8155a16273a7fa3a9d159014abd947 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Jul 2018 17:17:27 +0200 Subject: [PATCH 028/472] prototype support for lazily loading members in matrixclient --- src/base-apis.js | 12 ++++++++++++ src/client.js | 14 ++++++++++++++ src/models/room.js | 18 ++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/src/base-apis.js b/src/base-apis.js index bf2381790..921bec527 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -417,6 +417,18 @@ MatrixBaseApis.prototype.roomState = function(roomId, callback) { return this._http.authedRequest(callback, "GET", path); }; +/** + * @param {string} roomId + * @param {module:client.callback} callback Optional. + * @return {module:client.Promise} Resolves: TODO + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixBaseApis.prototype.joinedMembers = function(roomId, callback) { + const path = utils.encodeUri("/rooms/$roomId/joined_members", {$roomId: roomId}); + return this._http.authedRequest(callback, "GET", path); +}; + + /** * @param {string} groupId * @return {module:client.Promise} Resolves: Group summary object diff --git a/src/client.js b/src/client.js index ab8c79b5f..93d837a4b 100644 --- a/src/client.js +++ b/src/client.js @@ -757,6 +757,20 @@ MatrixClient.prototype.getRoom = function(roomId) { return this.store.getRoom(roomId); }; +/** + * Preloads the member list for the given room id, + * in case lazy loading of memberships is in use. + * @param {string} roomId The room ID + */ +MatrixClient.prototype.loadRoomMembersIfNeeded = function(roomId) { + const room = this.getRoom(roomId); + if (!room || !room.membersNeedLoading()) { + return; + } + const membersPromise = this.joinedMembers(roomId); + room.setLazilyLoadedMembers(membersPromise); +} + /** * Retrieve all known rooms. * @return {Room[]} A list of rooms, or an empty list if there is no data store. diff --git a/src/models/room.js b/src/models/room.js index 7e01eab64..0141c164c 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -171,7 +171,10 @@ function Room(roomId, opts) { // read by megolm; boolean value - null indicates "use global value" this._blacklistUnverifiedDevices = null; + // in case of lazy loading, to keep track of loading state + this._membersNeedLoading = true; } + utils.inherits(Room, EventEmitter); /** @@ -212,7 +215,22 @@ Room.prototype.getPendingEvents = function() { Room.prototype.getLiveTimeline = function() { return this.getUnfilteredTimelineSet().getLiveTimeline(); }; +/** + * Get the lazy loading state, whether loading is needed or not. + */ +Room.prototype.membersNeedLoading = function() { + return this._membersNeedLoading; +} +/** + * + */ +Room.prototype.setLazilyLoadedMembers = async function(joinedMembersPromise) { + this._membersNeedLoading = false; + const members = await joinedMembersPromise; + this.currentState.setJoinedMembers(members.joined); + //for all timelines > room state, call setJoinedMembers? +} /** * Reset the live timeline of all timelineSets, and start new ones. From 45d86fa27046aaa798bde1207b4b0533116f39ad Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 10 Jul 2018 17:44:31 +0200 Subject: [PATCH 029/472] 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 --- src/models/room-state.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/models/room-state.js b/src/models/room-state.js index d557f4e03..97e03fde7 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -263,13 +263,19 @@ RoomState.prototype.setJoinedMembers = function(joinedMembers) { const avatarUrl = details.avatar_url; const member = new RoomMember(this.roomId, userId); member.setAsJoinedMember(displayName, avatarUrl, this); - return member; + const isNewMember = !this.members[userId]; + return {member, isNewMember}; }); - joinedRoomMembers.forEach(member => { + joinedRoomMembers.forEach(({member, isNewMember}) => { _updateDisplayNameCache(this, member.userId, member.name); this._updateMember(member); + if (isNewMember) { + this.emit('RoomState.newMember', {}, self, member); + } + else { + this.emit('RoomState.members', {}, self, member); + } }); - this.emit("Room"); } /** From 759d415d4039e0db4c3ed41c87f29ad9cc855097 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 12 Jul 2018 19:34:27 +0200 Subject: [PATCH 030/472] preserve member state event if available when lazy loading members --- src/models/room-state.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/models/room-state.js b/src/models/room-state.js index 97e03fde7..cbcab88d2 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -262,6 +262,15 @@ RoomState.prototype.setJoinedMembers = function(joinedMembers) { const displayName = details.display_name; const avatarUrl = details.avatar_url; const member = new RoomMember(this.roomId, userId); + // try to find the member event for the user and set it first on the member + // so inspection of the event is possible later on if we have it + const membershipEvents = this.events["m.room.member"]; + const userMemberEvent = membershipEvents && membershipEvents[userId]; + if (userMemberEvent) { + member.setMembershipEvent(userMemberEvent, this); + } + // override the displayName and avatarUrl from the lazily loaded members + // as this is guaranteed to be the current state member.setAsJoinedMember(displayName, avatarUrl, this); const isNewMember = !this.members[userId]; return {member, isNewMember}; From 6c584d2b4c224087b0566075bf78c06d8eff343d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 12 Jul 2018 19:35:44 +0200 Subject: [PATCH 031/472] keep is_direct checks inside RoomMember as events.member might not be available --- src/models/room-member.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/models/room-member.js b/src/models/room-member.js index b52b6014b..49ef5d981 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -192,6 +192,37 @@ RoomMember.prototype.getLastModifiedTime = function() { return this._modified; }; + +/** + * 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.getDirectChatInviter = 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(); + + if (memberContent.membership === "join") { + memberContent = memberEvent.getPrevContent(); + } + + if (memberContent.membership === "invite" && memberContent.is_direct) { + return memberEvent.getUnsigned().prev_sender; + } + } +} + + /** * Get the avatar URL for a room member. * @param {string} baseUrl The base homeserver URL See From c917c4a468060b8ddded83673e281a0962c06287 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 13 Jul 2018 14:17:42 +0200 Subject: [PATCH 032/472] return correct invite sender in case of a join --- src/models/room-member.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/models/room-member.js b/src/models/room-member.js index 49ef5d981..6373bda96 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -59,6 +59,7 @@ function RoomMember(roomId, userId) { member: null, }; this._lazyLoadAvatarUrl = null; + this._isLazilyLoaded = false; this._updateModifiedTime(); } utils.inherits(RoomMember, EventEmitter); @@ -105,6 +106,7 @@ RoomMember.prototype.setAsJoinedMember = function(displayName, avatarUrl, roomSt this.name = calculateDisplayName(this.userId, displayName, roomState); this.rawDisplayName = displayName || this.userId; this._lazyLoadAvatarUrl = avatarUrl; + this._isLazilyLoaded = true; //TODO: race condition between existing membership events since started syncing } @@ -193,6 +195,11 @@ RoomMember.prototype.getLastModifiedTime = function() { }; +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 @@ -211,13 +218,15 @@ RoomMember.prototype.getDirectChatInviter = function() { 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 memberEvent.getUnsigned().prev_sender; + return inviteSender; } } } From 32b741e205d083261a857cb08efb9ba8914cc2b5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 13 Jul 2018 14:23:43 +0200 Subject: [PATCH 033/472] use more consistent naming --- src/models/room-member.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room-member.js b/src/models/room-member.js index 6373bda96..bd05ae249 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -205,7 +205,7 @@ RoomMember.prototype.isKicked = function() { * the user that invited this member * @return {string} user id of the inviter */ -RoomMember.prototype.getDirectChatInviter = function() { +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) { From 0ca3475878ad210615bd792e9aa8b5d2192c6c77 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 13 Jul 2018 15:57:29 +0200 Subject: [PATCH 034/472] make method to get mxc avatar url public so MemberInfo can use it and take lazy loading into account --- src/models/room-member.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/models/room-member.js b/src/models/room-member.js index bd05ae249..d424e4da2 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -256,7 +256,7 @@ RoomMember.prototype.getAvatarUrl = allowDefault = true; } - const rawUrl = this._getRawAvatarMxcUrl(); + const rawUrl = this.getMxcAvatarUrl(); if (!rawUrl && !allowDefault) { return null; @@ -277,11 +277,13 @@ RoomMember.prototype.getAvatarUrl = * get the mxc avatar url, either from a state event, or from a lazily loaded member * @return {string} the mxc avatar url */ -RoomMember.prototype._getRawAvatarMxcUrl = function() { +RoomMember.prototype.getMxcAvatarUrl = function() { if (this._lazyLoadAvatarUrl) { return this._lazyLoadAvatarUrl; } else if(this.events.member) { return this.events.member.getContent().avatar_url; + } else if(this.user) { + return this.user.avatarUrl; } return null; } From 809674ca2ba0bce19b41d4eeb6ba7e7e8b3a0d1e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 16 Jul 2018 12:45:00 +0200 Subject: [PATCH 035/472] set lazily loaded members on all RoomStates of a room for all timelines in all timeline sets --- src/models/event-timeline-set.js | 8 ++++++++ src/models/event-timeline.js | 9 +++++++++ src/models/room-state.js | 4 ++++ src/models/room.js | 6 +++--- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/models/event-timeline-set.js b/src/models/event-timeline-set.js index 2777ea313..5b7ade9c4 100644 --- a/src/models/event-timeline-set.js +++ b/src/models/event-timeline-set.js @@ -73,6 +73,14 @@ function EventTimelineSet(room, opts) { } utils.inherits(EventTimelineSet, EventEmitter); +/** + * Sets the lazily loaded members. For now only joined members. + * @param {Profile[]} array with {avatar_url, display_name } tuples + */ +EventTimelineSet.prototype.setJoinedMembers = function(joinedMembers) { + this._timelines.forEach(tl => tl.setJoinedMembers(joinedMembers)); +} + /** * Get the filter object this timeline set is filtered on, if any * @return {?Filter} the optional filter for this timelineSet diff --git a/src/models/event-timeline.js b/src/models/event-timeline.js index 9ddf84e31..3c8c7a2b4 100644 --- a/src/models/event-timeline.js +++ b/src/models/event-timeline.js @@ -114,6 +114,15 @@ EventTimeline.prototype.getRoomId = function() { return this._roomId; }; +/** + * Sets the lazily loaded members. For now only joined members. + * @param {Profile[]} array with {avatar_url, display_name } tuples + */ +EventTimeline.prototype.setJoinedMembers = function(joinedMembers) { + this._startState.setJoinedMembers(joinedMembers); + this._endState.setJoinedMembers(joinedMembers); +} + /** * Get the filter for this timeline's timelineSet (if any) * @return {Filter} filter diff --git a/src/models/room-state.js b/src/models/room-state.js index cbcab88d2..5b6fe6e79 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -257,6 +257,10 @@ RoomState.prototype._updateMember = function(member) { this._joinedMemberCount = null; } +/** + * Sets the lazily loaded members. For now only joined members. + * @param {Profile[]} array with {avatar_url, display_name } tuples + */ RoomState.prototype.setJoinedMembers = function(joinedMembers) { const joinedRoomMembers = Object.entries(joinedMembers).map(([userId, details]) => { const displayName = details.display_name; diff --git a/src/models/room.js b/src/models/room.js index 0141c164c..019a59c13 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -223,13 +223,13 @@ Room.prototype.membersNeedLoading = function() { } /** - * + * Sets the lazily loaded members from the result of calling /joined_members + * @param {Promise} promise with result of /joined_members endpoint */ Room.prototype.setLazilyLoadedMembers = async function(joinedMembersPromise) { this._membersNeedLoading = false; const members = await joinedMembersPromise; - this.currentState.setJoinedMembers(members.joined); - //for all timelines > room state, call setJoinedMembers? + this._timelineSets.forEach(tlSet => tlSet.setJoinedMembers(members.joined)); } /** From 20a1828fa568bca97bdd055aa969f86570de8fdc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 16 Jul 2018 12:45:44 +0200 Subject: [PATCH 036/472] make sentinels lazy loading compatible dont just rely on member events, but just copy the member --- src/models/room-state.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/models/room-state.js b/src/models/room-state.js index 5b6fe6e79..c2f0198a8 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -118,14 +118,12 @@ RoomState.prototype.getSentinelMember = function(userId) { let sentinel = this._sentinels[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) { + return null; } + sentinel = new RoomMember(); + Object.assign(sentinel, member); this._sentinels[userId] = sentinel; } return sentinel; From 30adefed074f5abd5ae86df9a6fd39479190de8d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 17 Jul 2018 18:25:12 +0200 Subject: [PATCH 037/472] return sentinels with userid if members haven't been loaded yet, better than braking timeline continuation --- src/models/room-state.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/models/room-state.js b/src/models/room-state.js index c2f0198a8..bd9651977 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -118,12 +118,11 @@ RoomState.prototype.getSentinelMember = function(userId) { let sentinel = this._sentinels[userId]; if (sentinel === undefined) { + sentinel = new RoomMember(this.roomId, userId); const member = this.members[userId]; - if (!member) { - return null; + if (member) { + Object.assign(sentinel, member); } - sentinel = new RoomMember(); - Object.assign(sentinel, member); this._sentinels[userId] = sentinel; } return sentinel; From 88f2f62945594efb67275c2cb62ede0fcc46d736 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 17 Jul 2018 18:26:42 +0200 Subject: [PATCH 038/472] 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. --- src/models/event-timeline-set.js | 44 ++++---------------- src/models/event-timeline.js | 32 +++++++++++++++ src/models/room-member.js | 4 ++ src/models/room-state.js | 70 ++++++++++++++++++++------------ 4 files changed, 87 insertions(+), 63 deletions(-) diff --git a/src/models/event-timeline-set.js b/src/models/event-timeline-set.js index 5b7ade9c4..e21cbe2f7 100644 --- a/src/models/event-timeline-set.js +++ b/src/models/event-timeline-set.js @@ -176,49 +176,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( diff --git a/src/models/event-timeline.js b/src/models/event-timeline.js index 3c8c7a2b4..0ac11d879 100644 --- a/src/models/event-timeline.js +++ b/src/models/event-timeline.js @@ -106,6 +106,38 @@ 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. + */ +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. + */ +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 diff --git a/src/models/room-member.js b/src/models/room-member.js index d424e4da2..354ff5a15 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -64,6 +64,10 @@ function RoomMember(roomId, userId) { } utils.inherits(RoomMember, EventEmitter); +RoomMember.prototype.isLazyLoaded = function() { + return this._isLazilyLoaded; +} + /** * Update this room member's membership event. May fire "RoomMember.name" if * this event updates this member's name. diff --git a/src/models/room-state.js b/src/models/room-state.js index bd9651977..6eaa74457 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -149,6 +149,24 @@ 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. + */ +RoomState.prototype.clone = function() { + const copy = new RoomState(this.roomId); + //freeze and pass all state events to copy + Object.values(this.events).forEach(eventsByStateKey => { + const eventsForType = Object.values(eventsByStateKey); + copy.setStateEvents(eventsForType); + }); + // clone lazily loaded members + const lazyLoadedMembers = Object.values(this.members).filter(member => member.isLazyLoaded()); + lazyLoadedMembers.forEach(m => { + copy._setJoinedMember(m.userId, m.rawDisplayName, m.getMxcAvatarUrl()); + }); + return copy; +}; + /** * Add an array of one or more state MatrixEvents, overwriting * any existing state with the same {type, stateKey} tuple. Will fire @@ -259,35 +277,35 @@ RoomState.prototype._updateMember = function(member) { * @param {Profile[]} array with {avatar_url, display_name } tuples */ RoomState.prototype.setJoinedMembers = function(joinedMembers) { - const joinedRoomMembers = Object.entries(joinedMembers).map(([userId, details]) => { - const displayName = details.display_name; - const avatarUrl = details.avatar_url; - const member = new RoomMember(this.roomId, userId); - // try to find the member event for the user and set it first on the member - // so inspection of the event is possible later on if we have it - const membershipEvents = this.events["m.room.member"]; - const userMemberEvent = membershipEvents && membershipEvents[userId]; - if (userMemberEvent) { - member.setMembershipEvent(userMemberEvent, this); - } - // override the displayName and avatarUrl from the lazily loaded members - // as this is guaranteed to be the current state - member.setAsJoinedMember(displayName, avatarUrl, this); - const isNewMember = !this.members[userId]; - return {member, isNewMember}; - }); - joinedRoomMembers.forEach(({member, isNewMember}) => { - _updateDisplayNameCache(this, member.userId, member.name); - this._updateMember(member); - if (isNewMember) { - this.emit('RoomState.newMember', {}, self, member); - } - else { - this.emit('RoomState.members', {}, self, member); - } + Object.entries(joinedMembers).forEach(([userId, details]) => { + this._setJoinedMember(userId, details.display_name, details.avatar_url); }); } +RoomState.prototype._setJoinedMember = function(userId, displayName, avatarUrl) { + const member = new RoomMember(this.roomId, userId); + // try to find the member event for the user and set it first on the member + // so inspection of the event is possible later on if we have it + const membershipEvents = this.events["m.room.member"]; + const userMemberEvent = membershipEvents && membershipEvents[userId]; + if (userMemberEvent) { + member.setMembershipEvent(userMemberEvent, this); + } + // override the displayName and avatarUrl from the lazily loaded members + // as this is guaranteed to be the current state + member.setAsJoinedMember(displayName, avatarUrl, this); + const isNewMember = !this.members[userId]; + + _updateDisplayNameCache(this, member.userId, member.name); + this._updateMember(member); + if (isNewMember) { + this.emit('RoomState.newMember', {}, self, member); + } + else { + this.emit('RoomState.members', {}, self, member); + } +} + /** * Set the current typing event for this room. * @param {MatrixEvent} event The typing event From 9a796f13833b032f741773d6395dad6fc4cf22f1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 18 Jul 2018 12:40:16 +0200 Subject: [PATCH 039/472] fix lint errors --- src/client.js | 2 +- src/models/event-timeline-set.js | 6 +++--- src/models/event-timeline.js | 18 +++++++++++++++--- src/models/room-member.js | 12 ++++++++---- src/models/room-state.js | 20 +++++++++++--------- src/models/room.js | 14 ++++++++++---- 6 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/client.js b/src/client.js index 93d837a4b..82c4e377b 100644 --- a/src/client.js +++ b/src/client.js @@ -769,7 +769,7 @@ MatrixClient.prototype.loadRoomMembersIfNeeded = function(roomId) { } const membersPromise = this.joinedMembers(roomId); room.setLazilyLoadedMembers(membersPromise); -} +}; /** * Retrieve all known rooms. diff --git a/src/models/event-timeline-set.js b/src/models/event-timeline-set.js index e21cbe2f7..b5adb9913 100644 --- a/src/models/event-timeline-set.js +++ b/src/models/event-timeline-set.js @@ -75,11 +75,11 @@ utils.inherits(EventTimelineSet, EventEmitter); /** * Sets the lazily loaded members. For now only joined members. - * @param {Profile[]} array with {avatar_url, display_name } tuples + * @param {Profile[]} joinedMembers array with {avatar_url, display_name } tuples */ EventTimelineSet.prototype.setJoinedMembers = function(joinedMembers) { - this._timelines.forEach(tl => tl.setJoinedMembers(joinedMembers)); -} + this._timelines.forEach((tl) => tl.setJoinedMembers(joinedMembers)); +}; /** * Get the filter object this timeline set is filtered on, if any diff --git a/src/models/event-timeline.js b/src/models/event-timeline.js index 0ac11d879..43b25b864 100644 --- a/src/models/event-timeline.js +++ b/src/models/event-timeline.js @@ -111,10 +111,16 @@ EventTimeline.prototype.initialiseState = function(stateEvents) { * 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); + 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 @@ -129,6 +135,12 @@ EventTimeline.prototype.forkLive = function(direction) { /** * 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); @@ -148,12 +160,12 @@ EventTimeline.prototype.getRoomId = function() { /** * Sets the lazily loaded members. For now only joined members. - * @param {Profile[]} array with {avatar_url, display_name } tuples + * @param {Profile[]} joinedMembers array with {avatar_url, display_name } tuples */ EventTimeline.prototype.setJoinedMembers = function(joinedMembers) { this._startState.setJoinedMembers(joinedMembers); this._endState.setJoinedMembers(joinedMembers); -} +}; /** * Get the filter for this timeline's timelineSet (if any) diff --git a/src/models/room-member.js b/src/models/room-member.js index 354ff5a15..0169622a5 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -66,7 +66,7 @@ utils.inherits(RoomMember, EventEmitter); RoomMember.prototype.isLazyLoaded = function() { return this._isLazilyLoaded; -} +}; /** * Update this room member's membership event. May fire "RoomMember.name" if @@ -102,8 +102,12 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) { this.emit("RoomMember.name", event, this, oldName); } }; + /** * Update this room member from a lazily loaded member + * @param {string} displayName + * @param {string} avatarUrl + * @param {RoomState} roomState the room state this member is part of, needed to disambiguate the display name */ RoomMember.prototype.setAsJoinedMember = function(displayName, avatarUrl, roomState) { this.membership = "join"; @@ -112,7 +116,7 @@ RoomMember.prototype.setAsJoinedMember = function(displayName, avatarUrl, roomSt this._lazyLoadAvatarUrl = avatarUrl; this._isLazilyLoaded = true; //TODO: race condition between existing membership events since started syncing -} +}; /** * Update this room member's power level event. May fire @@ -233,7 +237,7 @@ RoomMember.prototype.getDMInviter = function() { return inviteSender; } } -} +}; /** @@ -290,7 +294,7 @@ RoomMember.prototype.getMxcAvatarUrl = function() { return this.user.avatarUrl; } return null; -} +}; function calculateDisplayName(selfUserId, displayName, roomState) { if (!displayName) { diff --git a/src/models/room-state.js b/src/models/room-state.js index 6eaa74457..d3dc0908f 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -149,19 +149,21 @@ 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); //freeze and pass all state events to copy - Object.values(this.events).forEach(eventsByStateKey => { + Object.values(this.events).forEach((eventsByStateKey) => { const eventsForType = Object.values(eventsByStateKey); copy.setStateEvents(eventsForType); }); // clone lazily loaded members - const lazyLoadedMembers = Object.values(this.members).filter(member => member.isLazyLoaded()); - lazyLoadedMembers.forEach(m => { + const lazyLoadedMembers = Object.values(this.members) + .filter((member) => member.isLazyLoaded()); + lazyLoadedMembers.forEach((m) => { copy._setJoinedMember(m.userId, m.rawDisplayName, m.getMxcAvatarUrl()); }); return copy; @@ -270,17 +272,17 @@ RoomState.prototype._updateMember = function(member) { this.members[member.userId] = member; this._joinedMemberCount = null; -} +}; /** * Sets the lazily loaded members. For now only joined members. - * @param {Profile[]} array with {avatar_url, display_name } tuples + * @param {Profile[]} joinedMembers array with {avatar_url, display_name } tuples */ RoomState.prototype.setJoinedMembers = function(joinedMembers) { Object.entries(joinedMembers).forEach(([userId, details]) => { this._setJoinedMember(userId, details.display_name, details.avatar_url); }); -} +}; RoomState.prototype._setJoinedMember = function(userId, displayName, avatarUrl) { const member = new RoomMember(this.roomId, userId); @@ -295,7 +297,7 @@ RoomState.prototype._setJoinedMember = function(userId, displayName, avatarUrl) // as this is guaranteed to be the current state member.setAsJoinedMember(displayName, avatarUrl, this); const isNewMember = !this.members[userId]; - + _updateDisplayNameCache(this, member.userId, member.name); this._updateMember(member); if (isNewMember) { @@ -304,7 +306,7 @@ RoomState.prototype._setJoinedMember = function(userId, displayName, avatarUrl) else { this.emit('RoomState.members', {}, self, member); } -} +}; /** * Set the current typing event for this room. diff --git a/src/models/room.js b/src/models/room.js index 019a59c13..f4b33c427 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -215,22 +215,28 @@ Room.prototype.getPendingEvents = function() { Room.prototype.getLiveTimeline = function() { return this.getUnfilteredTimelineSet().getLiveTimeline(); }; + /** * Get the lazy loading state, whether loading is needed or not. + * @return {bool} whether or not the members of this room need to be loaded */ Room.prototype.membersNeedLoading = function() { return this._membersNeedLoading; -} +}; /** * Sets the lazily loaded members from the result of calling /joined_members - * @param {Promise} promise with result of /joined_members endpoint + * @param {Promise} joinedMembersPromise promise with result of /joined_members endpoint */ Room.prototype.setLazilyLoadedMembers = async function(joinedMembersPromise) { this._membersNeedLoading = false; const members = await joinedMembersPromise; - this._timelineSets.forEach(tlSet => tlSet.setJoinedMembers(members.joined)); -} + //wait 10 seconds + await new Promise((resolve) => setTimeout(resolve, 5000)); + console.log('set lazily loaded members!'); + this._timelineSets.forEach((tlSet) => tlSet.setJoinedMembers(members.joined)); + this.emit('Room', this); +}; /** * Reset the live timeline of all timelineSets, and start new ones. From abd15748ceb5f47ed89f45e7a1ec0d33fa7c567a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 18 Jul 2018 12:46:58 +0200 Subject: [PATCH 040/472] fix one lint warning too many --- src/models/room-state.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/models/room-state.js b/src/models/room-state.js index d3dc0908f..acb34daea 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -302,8 +302,7 @@ RoomState.prototype._setJoinedMember = function(userId, displayName, avatarUrl) this._updateMember(member); if (isNewMember) { this.emit('RoomState.newMember', {}, self, member); - } - else { + } else { this.emit('RoomState.members', {}, self, member); } }; From 8733654094cf8aeb5920d64d8c362bfa2f3344cd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 19 Jul 2018 09:40:42 +0200 Subject: [PATCH 041/472] remove left-over test code, oops --- src/models/room.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index f4b33c427..11c9e9fbe 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -231,9 +231,6 @@ Room.prototype.membersNeedLoading = function() { Room.prototype.setLazilyLoadedMembers = async function(joinedMembersPromise) { this._membersNeedLoading = false; const members = await joinedMembersPromise; - //wait 10 seconds - await new Promise((resolve) => setTimeout(resolve, 5000)); - console.log('set lazily loaded members!'); this._timelineSets.forEach((tlSet) => tlSet.setJoinedMembers(members.joined)); this.emit('Room', this); }; From 2155dd05527f37cbf1bd5b3b164215483af7f839 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 19 Jul 2018 09:40:58 +0200 Subject: [PATCH 042/472] improve return type comment --- src/base-apis.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base-apis.js b/src/base-apis.js index 921bec527..624a81681 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -420,7 +420,7 @@ MatrixBaseApis.prototype.roomState = function(roomId, callback) { /** * @param {string} roomId * @param {module:client.callback} callback Optional. - * @return {module:client.Promise} Resolves: TODO + * @return {module:client.Promise} Resolves: dictionary of userid to profile information * @return {module:http-api.MatrixError} Rejects: with an error response. */ MatrixBaseApis.prototype.joinedMembers = function(roomId, callback) { From 2097b31d4fd62ac4da084dbd4f227e8401f458a1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 19 Jul 2018 10:08:22 +0200 Subject: [PATCH 043/472] handle failed /joined_members call by logging to console and reverting flag --- src/models/room.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/models/room.js b/src/models/room.js index 11c9e9fbe..edc6dbace 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -230,7 +230,16 @@ Room.prototype.membersNeedLoading = function() { */ Room.prototype.setLazilyLoadedMembers = async function(joinedMembersPromise) { this._membersNeedLoading = false; - const members = await joinedMembersPromise; + let members = null; + try { + members = await joinedMembersPromise; + } + catch (err) { + console.error(`Fetching room members for ${this.roomId} failed. Room members will appear incomplete.`); + console.error(err); + this._membersNeedLoading = true; + return; + } this._timelineSets.forEach((tlSet) => tlSet.setJoinedMembers(members.joined)); this.emit('Room', this); }; From 1c81a17298bb193ace45c559c5f9d93310fd8564 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 19 Jul 2018 10:14:44 +0200 Subject: [PATCH 044/472] 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. --- spec/unit/room-state.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/unit/room-state.spec.js b/spec/unit/room-state.spec.js index 66e7df1be..3cbad0e96 100644 --- a/spec/unit/room-state.spec.js +++ b/spec/unit/room-state.spec.js @@ -78,8 +78,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", From ba34a766e7fa336e3de372e30c6f3fc6d69feabe Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 19 Jul 2018 10:21:32 +0200 Subject: [PATCH 045/472] fix lint --- src/models/room.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index edc6dbace..8e0511f2a 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -233,9 +233,10 @@ Room.prototype.setLazilyLoadedMembers = async function(joinedMembersPromise) { let members = null; try { members = await joinedMembersPromise; - } - catch (err) { - console.error(`Fetching room members for ${this.roomId} failed. Room members will appear incomplete.`); + } catch (err) { + const errorMessage = `Fetching room members for ${this.roomId} failed.` + + " Room members will appear incomplete."; + console.error(errorMessage); console.error(err); this._membersNeedLoading = true; return; From 6061deac37c73a07705c68a4397629d856b938b4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 19 Jul 2018 14:55:36 +0200 Subject: [PATCH 046/472] use method for getting state event, less code --- src/models/room-state.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/models/room-state.js b/src/models/room-state.js index acb34daea..5e29b101c 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -288,8 +288,7 @@ RoomState.prototype._setJoinedMember = function(userId, displayName, avatarUrl) const member = new RoomMember(this.roomId, userId); // try to find the member event for the user and set it first on the member // so inspection of the event is possible later on if we have it - const membershipEvents = this.events["m.room.member"]; - const userMemberEvent = membershipEvents && membershipEvents[userId]; + const userMemberEvent = this.getStateEvents("m.room.member", userId); if (userMemberEvent) { member.setMembershipEvent(userMemberEvent, this); } From d95d44dc948516ae0fab8cd3cf9acae3fa401652 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 19 Jul 2018 15:04:05 +0200 Subject: [PATCH 047/472] move error handling to caller --- src/client.js | 4 ++-- src/models/room.js | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/client.js b/src/client.js index 82c4e377b..6f794e73b 100644 --- a/src/client.js +++ b/src/client.js @@ -762,13 +762,13 @@ MatrixClient.prototype.getRoom = function(roomId) { * in case lazy loading of memberships is in use. * @param {string} roomId The room ID */ -MatrixClient.prototype.loadRoomMembersIfNeeded = function(roomId) { +MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { const room = this.getRoom(roomId); if (!room || !room.membersNeedLoading()) { return; } const membersPromise = this.joinedMembers(roomId); - room.setLazilyLoadedMembers(membersPromise); + await room.setLazilyLoadedMembers(membersPromise); }; /** diff --git a/src/models/room.js b/src/models/room.js index 8e0511f2a..b577a1047 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -234,12 +234,8 @@ Room.prototype.setLazilyLoadedMembers = async function(joinedMembersPromise) { try { members = await joinedMembersPromise; } catch (err) { - const errorMessage = `Fetching room members for ${this.roomId} failed.` + - " Room members will appear incomplete."; - console.error(errorMessage); - console.error(err); this._membersNeedLoading = true; - return; + throw err; //rethrow so calling code is aware operation failed } this._timelineSets.forEach((tlSet) => tlSet.setJoinedMembers(members.joined)); this.emit('Room', this); From 5de0d3955392ec82f1843248afae0b6b1ba08f46 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 19 Jul 2018 15:21:51 +0200 Subject: [PATCH 048/472] 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. --- src/client.js | 13 ++++++++++++- src/models/event-timeline-set.js | 8 ++++---- src/models/event-timeline.js | 10 +++++----- src/models/room-member.js | 13 ++++++------- src/models/room-state.js | 26 ++++++++++++++------------ src/models/room.js | 8 ++++---- 6 files changed, 45 insertions(+), 33 deletions(-) diff --git a/src/client.js b/src/client.js index 6f794e73b..4612d920f 100644 --- a/src/client.js +++ b/src/client.js @@ -767,7 +767,18 @@ MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { if (!room || !room.membersNeedLoading()) { return; } - const membersPromise = this.joinedMembers(roomId); + const joinedMembersPromise = this.joinedMembers(roomId); + const membersPromise = joinedMembersPromise.then((profiles) => { + return Object.entries(profiles).map(([userId, profile]) => { + return { + userId: userId, + avatarUrl: profile.avatar_url, + displayName: profile.display_name, + membership: "join", // as we need to support invitees as well + // in the future, already include but hardcode it + }; + }); + }); await room.setLazilyLoadedMembers(membersPromise); }; diff --git a/src/models/event-timeline-set.js b/src/models/event-timeline-set.js index b5adb9913..e9cfc86b0 100644 --- a/src/models/event-timeline-set.js +++ b/src/models/event-timeline-set.js @@ -74,11 +74,11 @@ function EventTimelineSet(room, opts) { utils.inherits(EventTimelineSet, EventEmitter); /** - * Sets the lazily loaded members. For now only joined members. - * @param {Profile[]} joinedMembers array with {avatar_url, display_name } tuples + * Sets the lazily loaded members. + * @param {Member[]} members array of {userId, avatarUrl, displayName, membership} tuples */ -EventTimelineSet.prototype.setJoinedMembers = function(joinedMembers) { - this._timelines.forEach((tl) => tl.setJoinedMembers(joinedMembers)); +EventTimelineSet.prototype.setLazilyLoadedMembers = function(members) { + this._timelines.forEach((tl) => tl.setLazilyLoadedMembers(members)); }; /** diff --git a/src/models/event-timeline.js b/src/models/event-timeline.js index 43b25b864..7afeb4063 100644 --- a/src/models/event-timeline.js +++ b/src/models/event-timeline.js @@ -159,12 +159,12 @@ EventTimeline.prototype.getRoomId = function() { }; /** - * Sets the lazily loaded members. For now only joined members. - * @param {Profile[]} joinedMembers array with {avatar_url, display_name } tuples + * Sets the lazily loaded members. + * @param {Member[]} members array of {userId, avatarUrl, displayName, membership} tuples */ -EventTimeline.prototype.setJoinedMembers = function(joinedMembers) { - this._startState.setJoinedMembers(joinedMembers); - this._endState.setJoinedMembers(joinedMembers); +EventTimeline.prototype.setLazilyLoadedMembers = function(members) { + this._startState.setLazilyLoadedMembers(members); + this._endState.setLazilyLoadedMembers(members); }; /** diff --git a/src/models/room-member.js b/src/models/room-member.js index 0169622a5..060f9db8e 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -105,15 +105,14 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) { /** * Update this room member from a lazily loaded member - * @param {string} displayName - * @param {string} avatarUrl + * @param {Member} memberInfo a {userId, avatarUrl, displayName, membership} tuple * @param {RoomState} roomState the room state this member is part of, needed to disambiguate the display name */ -RoomMember.prototype.setAsJoinedMember = function(displayName, avatarUrl, roomState) { - this.membership = "join"; - this.name = calculateDisplayName(this.userId, displayName, roomState); - this.rawDisplayName = displayName || this.userId; - this._lazyLoadAvatarUrl = avatarUrl; +RoomMember.prototype.setAsLazilyLoadedMember = function(memberInfo, roomState) { + this.membership = memberInfo.membership; + this.name = calculateDisplayName(this.userId, memberInfo.displayName, roomState); + this.rawDisplayName = memberInfo.displayName || this.userId; + this._lazyLoadAvatarUrl = memberInfo.avatarUrl; this._isLazilyLoaded = true; //TODO: race condition between existing membership events since started syncing }; diff --git a/src/models/room-state.js b/src/models/room-state.js index 5e29b101c..5c2f6ef0d 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -275,27 +275,29 @@ RoomState.prototype._updateMember = function(member) { }; /** - * Sets the lazily loaded members. For now only joined members. - * @param {Profile[]} joinedMembers array with {avatar_url, display_name } tuples + * Sets the lazily loaded members. + * @param {Member[]} members array of {userId, avatarUrl, displayName, membership} tuples */ -RoomState.prototype.setJoinedMembers = function(joinedMembers) { - Object.entries(joinedMembers).forEach(([userId, details]) => { - this._setJoinedMember(userId, details.display_name, details.avatar_url); - }); +RoomState.prototype.setLazilyLoadedMembers = function(members) { + members.forEach((member) => this._setLazilyLoadedMember(member)); }; - -RoomState.prototype._setJoinedMember = function(userId, displayName, avatarUrl) { - const member = new RoomMember(this.roomId, userId); +/** + * Add/updates a lazily loaded member. + * @param {Member} memberInfo a {userId, avatarUrl, displayName, membership} tuple + */ +RoomState.prototype._setLazilyLoadedMember = function(memberInfo) { + const member = new RoomMember(this.roomId, memberInfo.userId); // try to find the member event for the user and set it first on the member // so inspection of the event is possible later on if we have it - const userMemberEvent = this.getStateEvents("m.room.member", userId); + const userMemberEvent = this.getStateEvents("m.room.member", member.userId); if (userMemberEvent) { member.setMembershipEvent(userMemberEvent, this); } // override the displayName and avatarUrl from the lazily loaded members // as this is guaranteed to be the current state - member.setAsJoinedMember(displayName, avatarUrl, this); - const isNewMember = !this.members[userId]; + member.setAsLazilyLoadedMember(memberInfo, this); + + const isNewMember = !this.members[member.userId]; _updateDisplayNameCache(this, member.userId, member.name); this._updateMember(member); diff --git a/src/models/room.js b/src/models/room.js index b577a1047..da7485962 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -226,18 +226,18 @@ Room.prototype.membersNeedLoading = function() { /** * Sets the lazily loaded members from the result of calling /joined_members - * @param {Promise} joinedMembersPromise promise with result of /joined_members endpoint + * @param {Promise} membersPromise promise with array of {userId, avatarUrl, displayName, membership} tuples */ -Room.prototype.setLazilyLoadedMembers = async function(joinedMembersPromise) { +Room.prototype.setLazilyLoadedMembers = async function(membersPromise) { this._membersNeedLoading = false; let members = null; try { - members = await joinedMembersPromise; + members = await membersPromise; } catch (err) { this._membersNeedLoading = true; throw err; //rethrow so calling code is aware operation failed } - this._timelineSets.forEach((tlSet) => tlSet.setJoinedMembers(members.joined)); + this._timelineSets.forEach((tlSet) => tlSet.setLazilyLoadedMembers(members)); this.emit('Room', this); }; From 5e30aff4183cf2248c3ab91c45e7fe8b9144e9fc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 19 Jul 2018 15:24:57 +0200 Subject: [PATCH 049/472] more consistent naming --- src/client.js | 2 +- src/models/event-timeline-set.js | 4 ++-- src/models/event-timeline.js | 6 +++--- src/models/room-member.js | 8 ++++---- src/models/room-state.js | 8 ++++---- src/models/room.js | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/client.js b/src/client.js index 4612d920f..3e39691b1 100644 --- a/src/client.js +++ b/src/client.js @@ -779,7 +779,7 @@ MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { }; }); }); - await room.setLazilyLoadedMembers(membersPromise); + await room.setLazyLoadedMembers(membersPromise); }; /** diff --git a/src/models/event-timeline-set.js b/src/models/event-timeline-set.js index e9cfc86b0..4264a40a2 100644 --- a/src/models/event-timeline-set.js +++ b/src/models/event-timeline-set.js @@ -77,8 +77,8 @@ utils.inherits(EventTimelineSet, EventEmitter); * Sets the lazily loaded members. * @param {Member[]} members array of {userId, avatarUrl, displayName, membership} tuples */ -EventTimelineSet.prototype.setLazilyLoadedMembers = function(members) { - this._timelines.forEach((tl) => tl.setLazilyLoadedMembers(members)); +EventTimelineSet.prototype.setLazyLoadedMembers = function(members) { + this._timelines.forEach((tl) => tl.setLazyLoadedMembers(members)); }; /** diff --git a/src/models/event-timeline.js b/src/models/event-timeline.js index 7afeb4063..c6f21bd0c 100644 --- a/src/models/event-timeline.js +++ b/src/models/event-timeline.js @@ -162,9 +162,9 @@ EventTimeline.prototype.getRoomId = function() { * Sets the lazily loaded members. * @param {Member[]} members array of {userId, avatarUrl, displayName, membership} tuples */ -EventTimeline.prototype.setLazilyLoadedMembers = function(members) { - this._startState.setLazilyLoadedMembers(members); - this._endState.setLazilyLoadedMembers(members); +EventTimeline.prototype.setLazyLoadedMembers = function(members) { + this._startState.setLazyLoadedMembers(members); + this._endState.setLazyLoadedMembers(members); }; /** diff --git a/src/models/room-member.js b/src/models/room-member.js index 060f9db8e..4f23101ae 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -59,13 +59,13 @@ function RoomMember(roomId, userId) { member: null, }; this._lazyLoadAvatarUrl = null; - this._isLazilyLoaded = false; + this._isLazyLoaded = false; this._updateModifiedTime(); } utils.inherits(RoomMember, EventEmitter); RoomMember.prototype.isLazyLoaded = function() { - return this._isLazilyLoaded; + return this._isLazyLoaded; }; /** @@ -108,12 +108,12 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) { * @param {Member} memberInfo a {userId, avatarUrl, displayName, membership} tuple * @param {RoomState} roomState the room state this member is part of, needed to disambiguate the display name */ -RoomMember.prototype.setAsLazilyLoadedMember = function(memberInfo, roomState) { +RoomMember.prototype.setAsLazyLoadedMember = function(memberInfo, roomState) { this.membership = memberInfo.membership; this.name = calculateDisplayName(this.userId, memberInfo.displayName, roomState); this.rawDisplayName = memberInfo.displayName || this.userId; this._lazyLoadAvatarUrl = memberInfo.avatarUrl; - this._isLazilyLoaded = true; + this._isLazyLoaded = true; //TODO: race condition between existing membership events since started syncing }; diff --git a/src/models/room-state.js b/src/models/room-state.js index 5c2f6ef0d..1cbd545da 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -278,14 +278,14 @@ RoomState.prototype._updateMember = function(member) { * Sets the lazily loaded members. * @param {Member[]} members array of {userId, avatarUrl, displayName, membership} tuples */ -RoomState.prototype.setLazilyLoadedMembers = function(members) { - members.forEach((member) => this._setLazilyLoadedMember(member)); +RoomState.prototype.setLazyLoadedMembers = function(members) { + members.forEach((member) => this._setLazyLoadedMember(member)); }; /** * Add/updates a lazily loaded member. * @param {Member} memberInfo a {userId, avatarUrl, displayName, membership} tuple */ -RoomState.prototype._setLazilyLoadedMember = function(memberInfo) { +RoomState.prototype._setLazyLoadedMember = function(memberInfo) { const member = new RoomMember(this.roomId, memberInfo.userId); // try to find the member event for the user and set it first on the member // so inspection of the event is possible later on if we have it @@ -295,7 +295,7 @@ RoomState.prototype._setLazilyLoadedMember = function(memberInfo) { } // override the displayName and avatarUrl from the lazily loaded members // as this is guaranteed to be the current state - member.setAsLazilyLoadedMember(memberInfo, this); + member.setAsLazyLoadedMember(memberInfo, this); const isNewMember = !this.members[member.userId]; diff --git a/src/models/room.js b/src/models/room.js index da7485962..8b5f76ae0 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -228,7 +228,7 @@ Room.prototype.membersNeedLoading = function() { * Sets the lazily loaded members from the result of calling /joined_members * @param {Promise} membersPromise promise with array of {userId, avatarUrl, displayName, membership} tuples */ -Room.prototype.setLazilyLoadedMembers = async function(membersPromise) { +Room.prototype.setLazyLoadedMembers = async function(membersPromise) { this._membersNeedLoading = false; let members = null; try { @@ -237,7 +237,7 @@ Room.prototype.setLazilyLoadedMembers = async function(membersPromise) { this._membersNeedLoading = true; throw err; //rethrow so calling code is aware operation failed } - this._timelineSets.forEach((tlSet) => tlSet.setLazilyLoadedMembers(members)); + this._timelineSets.forEach((tlSet) => tlSet.setLazyLoadedMembers(members)); this.emit('Room', this); }; From 008d85ed324909f0098f908bd1180d93f1c70c01 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 19 Jul 2018 16:04:04 +0200 Subject: [PATCH 050/472] pick joined property out of response --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 3e39691b1..e2bf224c2 100644 --- a/src/client.js +++ b/src/client.js @@ -769,7 +769,7 @@ MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { } const joinedMembersPromise = this.joinedMembers(roomId); const membersPromise = joinedMembersPromise.then((profiles) => { - return Object.entries(profiles).map(([userId, profile]) => { + return Object.entries(profiles.joined).map(([userId, profile]) => { return { userId: userId, avatarUrl: profile.avatar_url, From de3f75bc835ce2ae2b3d49e84a7cf778981fb374 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 19 Jul 2018 17:17:55 +0200 Subject: [PATCH 051/472] Lazy loaded members should never take precendence over members acquired through state events --- src/models/room-member.js | 4 ++++ src/models/room-state.js | 35 +++++++++-------------------------- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/models/room-member.js b/src/models/room-member.js index 4f23101ae..78901d5d5 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -81,6 +81,10 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) { if (event.getType() !== "m.room.member") { return; } + + this._lazyLoadAvatarUrl = null; + this._isLazyLoaded = false; + this.events.member = event; const oldMembership = this.membership; diff --git a/src/models/room-state.js b/src/models/room-state.js index 1cbd545da..8805d53ac 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -279,33 +279,16 @@ RoomState.prototype._updateMember = function(member) { * @param {Member[]} members array of {userId, avatarUrl, displayName, membership} tuples */ RoomState.prototype.setLazyLoadedMembers = function(members) { - members.forEach((member) => this._setLazyLoadedMember(member)); -}; -/** - * Add/updates a lazily loaded member. - * @param {Member} memberInfo a {userId, avatarUrl, displayName, membership} tuple - */ -RoomState.prototype._setLazyLoadedMember = function(memberInfo) { - const member = new RoomMember(this.roomId, memberInfo.userId); - // try to find the member event for the user and set it first on the member - // so inspection of the event is possible later on if we have it - const userMemberEvent = this.getStateEvents("m.room.member", member.userId); - if (userMemberEvent) { - member.setMembershipEvent(userMemberEvent, this); - } - // override the displayName and avatarUrl from the lazily loaded members - // as this is guaranteed to be the current state - member.setAsLazyLoadedMember(memberInfo, this); - - const isNewMember = !this.members[member.userId]; - - _updateDisplayNameCache(this, member.userId, member.name); - this._updateMember(member); - if (isNewMember) { + let newMembers = members.filter((m) => !this.members.hasOwnProperty(m.userId)); + newMembers.forEach((memberInfo) => { + const member = new RoomMember(this.roomId, memberInfo.userId); + // override the displayName and avatarUrl from the lazily loaded members + // as this is guaranteed to be the current state + member.setAsLazyLoadedMember(memberInfo, this); + _updateDisplayNameCache(this, member.userId, member.name); + this._updateMember(member); this.emit('RoomState.newMember', {}, self, member); - } else { - this.emit('RoomState.members', {}, self, member); - } + }); }; /** From 8ad2a94a9051e5c9321eb5779e13d56621c75888 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 19 Jul 2018 17:34:02 +0200 Subject: [PATCH 052/472] make sure LL member doesn't override state event extra safety check, as this should already not happen because of the check in RoomState --- src/models/room-member.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/models/room-member.js b/src/models/room-member.js index 78901d5d5..3accbc29e 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -113,12 +113,14 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) { * @param {RoomState} roomState the room state this member is part of, needed to disambiguate the display name */ RoomMember.prototype.setAsLazyLoadedMember = function(memberInfo, roomState) { + if (this.events.member) { + return; + } this.membership = memberInfo.membership; this.name = calculateDisplayName(this.userId, memberInfo.displayName, roomState); this.rawDisplayName = memberInfo.displayName || this.userId; this._lazyLoadAvatarUrl = memberInfo.avatarUrl; this._isLazyLoaded = true; - //TODO: race condition between existing membership events since started syncing }; /** From 1ce580bba3158336f750335ef96d1b67f6e6600e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 19 Jul 2018 17:59:29 +0200 Subject: [PATCH 053/472] test lazy loaded info is returned and then discarded when setting a state event --- spec/unit/room-member.spec.js | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/spec/unit/room-member.spec.js b/spec/unit/room-member.spec.js index c36bb281c..299e32356 100644 --- a/spec/unit/room-member.spec.js +++ b/spec/unit/room-member.spec.js @@ -192,6 +192,50 @@ describe("RoomMember", function() { }); }); + describe("setAsLazyLoadedMember", function() { + const hsUrl = "https://my.home.server"; + const lazyUserId = "@lazy:bar"; + const displayName = "Mr. Lazy"; + const memberInfo = { + avatarUrl: "mxc://lazy/loaded", + displayName: displayName , + membership: "join" + }; + it("should allow precedence of state events", + function() { + const member = new RoomMember(roomId, lazyUserId); + member.setAsLazyLoadedMember(memberInfo); + let url = member.getAvatarUrl(hsUrl); + expect(url.indexOf("lazy/loaded")).toNotEqual(-1); + expect(member.name).toEqual(displayName); + expect(member.isLazyLoaded()).toEqual(true); + + member.setMembershipEvent(utils.mkEvent({ + event: true, + type: "m.room.member", + skey: lazyUserId, + room: roomId, + user: lazyUserId, + content: { + displayname: "Mr. State", + membership: "join", + avatar_url: "mxc://flibble/wibble", + }, + })); + + url = member.getAvatarUrl(hsUrl); + + // check that the member can't be set as lazy loaded anymore + // once it has a member event + for(let i = 0; i <= 2; ++i) { + expect(url.indexOf("flibble/wibble")).toNotEqual(-1); + expect(member.name).toEqual("Mr. State"); + expect(member.isLazyLoaded()).toEqual(false); + member.setAsLazyLoadedMember(memberInfo); + } + }); + }); + describe("setMembershipEvent", function() { const joinEvent = utils.mkMembership({ event: true, From 9ec6ea3bdfb55e2322b37741bbfbe935011b4ea0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 09:45:21 +0200 Subject: [PATCH 054/472] 2, not 3 times --- spec/unit/room-member.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/room-member.spec.js b/spec/unit/room-member.spec.js index 299e32356..c0f1c7e56 100644 --- a/spec/unit/room-member.spec.js +++ b/spec/unit/room-member.spec.js @@ -227,7 +227,7 @@ describe("RoomMember", function() { // check that the member can't be set as lazy loaded anymore // once it has a member event - for(let i = 0; i <= 2; ++i) { + for(let i = 0; i < 2; ++i) { expect(url.indexOf("flibble/wibble")).toNotEqual(-1); expect(member.name).toEqual("Mr. State"); expect(member.isLazyLoaded()).toEqual(false); From 0d2ac42dc4c16d2985eb053927ba75890c97b1f1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 13:32:26 +0200 Subject: [PATCH 055/472] add comment to clarify corners cut in prototype --- src/client.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/client.js b/src/client.js index e2bf224c2..6f8d2251a 100644 --- a/src/client.js +++ b/src/client.js @@ -768,6 +768,14 @@ MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { return; } const joinedMembersPromise = this.joinedMembers(roomId); + // XXX: we should make sure that the members we get back represent the + // room state at a given point in time. The plan is to do this by + // passing the current next_batch sync token to the endpoint we use + // to fetch the members. For now, this is a prototype that uses + // the /joined_members api, which does not support this synchronization, + // so there is a race condition here between the current /sync call + // and the /joined_members call: if the have conflicting information, which one + // represents the most recent state? const membersPromise = joinedMembersPromise.then((profiles) => { return Object.entries(profiles.joined).map(([userId, profile]) => { return { From 531ccf18190e9a1909485ef6d0218025bd7cbb19 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 13:33:36 +0200 Subject: [PATCH 056/472] actually, comment should be 1 line higher --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 6f8d2251a..8084f00a0 100644 --- a/src/client.js +++ b/src/client.js @@ -767,7 +767,6 @@ MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { if (!room || !room.membersNeedLoading()) { return; } - const joinedMembersPromise = this.joinedMembers(roomId); // XXX: we should make sure that the members we get back represent the // room state at a given point in time. The plan is to do this by // passing the current next_batch sync token to the endpoint we use @@ -776,6 +775,7 @@ MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { // so there is a race condition here between the current /sync call // and the /joined_members call: if the have conflicting information, which one // represents the most recent state? + const joinedMembersPromise = this.joinedMembers(roomId); const membersPromise = joinedMembersPromise.then((profiles) => { return Object.entries(profiles.joined).map(([userId, profile]) => { return { From 031f7225409626e2c1e41ff1049d07fa6e8c4b66 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 13:39:15 +0200 Subject: [PATCH 057/472] clarify that we only get joined members for now --- src/client.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/client.js b/src/client.js index 8084f00a0..63cf770cd 100644 --- a/src/client.js +++ b/src/client.js @@ -771,10 +771,16 @@ MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { // room state at a given point in time. The plan is to do this by // passing the current next_batch sync token to the endpoint we use // to fetch the members. For now, this is a prototype that uses - // the /joined_members api, which does not support this synchronization, - // so there is a race condition here between the current /sync call + // the /joined_members api, which only tells us about the joined members + // (not invites for example) and does not support this synchronization. + // So there is a race condition here between the current /sync call // and the /joined_members call: if the have conflicting information, which one // represents the most recent state? + // + // Addressing this race condition and the fact that this only tells us about + // joined members is a prerequisite for taking this out of the prototype stage and + // enabling the feature flag (feature_lazyloading) that + // the call to this method is behind. const joinedMembersPromise = this.joinedMembers(roomId); const membersPromise = joinedMembersPromise.then((profiles) => { return Object.entries(profiles.joined).map(([userId, profile]) => { From 79fcc9f343132d4844590ba520c7b8bb329d5da6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 14:05:28 +0200 Subject: [PATCH 058/472] 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. --- src/models/event-timeline-set.js | 8 -------- src/models/event-timeline.js | 9 --------- src/models/room.js | 2 +- 3 files changed, 1 insertion(+), 18 deletions(-) diff --git a/src/models/event-timeline-set.js b/src/models/event-timeline-set.js index 4264a40a2..65e15dfc8 100644 --- a/src/models/event-timeline-set.js +++ b/src/models/event-timeline-set.js @@ -73,14 +73,6 @@ function EventTimelineSet(room, opts) { } utils.inherits(EventTimelineSet, EventEmitter); -/** - * Sets the lazily loaded members. - * @param {Member[]} members array of {userId, avatarUrl, displayName, membership} tuples - */ -EventTimelineSet.prototype.setLazyLoadedMembers = function(members) { - this._timelines.forEach((tl) => tl.setLazyLoadedMembers(members)); -}; - /** * Get the filter object this timeline set is filtered on, if any * @return {?Filter} the optional filter for this timelineSet diff --git a/src/models/event-timeline.js b/src/models/event-timeline.js index c6f21bd0c..d23e483da 100644 --- a/src/models/event-timeline.js +++ b/src/models/event-timeline.js @@ -158,15 +158,6 @@ EventTimeline.prototype.getRoomId = function() { return this._roomId; }; -/** - * Sets the lazily loaded members. - * @param {Member[]} members array of {userId, avatarUrl, displayName, membership} tuples - */ -EventTimeline.prototype.setLazyLoadedMembers = function(members) { - this._startState.setLazyLoadedMembers(members); - this._endState.setLazyLoadedMembers(members); -}; - /** * Get the filter for this timeline's timelineSet (if any) * @return {Filter} filter diff --git a/src/models/room.js b/src/models/room.js index 8b5f76ae0..a5cade4e6 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -237,7 +237,7 @@ Room.prototype.setLazyLoadedMembers = async function(membersPromise) { this._membersNeedLoading = true; throw err; //rethrow so calling code is aware operation failed } - this._timelineSets.forEach((tlSet) => tlSet.setLazyLoadedMembers(members)); + this.currentState.setLazyLoadedMembers(members); this.emit('Room', this); }; From 0234f11914766799f3128a1c9752ab57fd176a20 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 14:49:09 +0200 Subject: [PATCH 059/472] some tests for room member + state, and some fixes to make them pass --- spec/unit/room-member.spec.js | 2 +- spec/unit/room-state.spec.js | 86 +++++++++++++++++++++++++++++++++++ src/models/room-member.js | 10 ++-- src/models/room-state.js | 44 +++++++++++++----- 4 files changed, 125 insertions(+), 17 deletions(-) diff --git a/spec/unit/room-member.spec.js b/spec/unit/room-member.spec.js index c0f1c7e56..7fc7af191 100644 --- a/spec/unit/room-member.spec.js +++ b/spec/unit/room-member.spec.js @@ -204,7 +204,7 @@ describe("RoomMember", function() { it("should allow precedence of state events", function() { const member = new RoomMember(roomId, lazyUserId); - member.setAsLazyLoadedMember(memberInfo); + member.setAsLazyLoadedMember(memberInfo.displayName, memberInfo.avatarUrl, memberInfo.membership); let url = member.getAvatarUrl(hsUrl); expect(url.indexOf("lazy/loaded")).toNotEqual(-1); expect(member.name).toEqual(displayName); diff --git a/spec/unit/room-state.spec.js b/spec/unit/room-state.spec.js index 3cbad0e96..4bd085908 100644 --- a/spec/unit/room-state.spec.js +++ b/spec/unit/room-state.spec.js @@ -11,6 +11,7 @@ describe("RoomState", function() { const roomId = "!foo:bar"; const userA = "@alice:bar"; const userB = "@bob:bar"; + const userLazy = "@lazy:bar"; let state; beforeEach(function() { @@ -263,6 +264,91 @@ describe("RoomState", function() { }); }); + describe("setLazyLoadedMembers", function() { + const mrLazy = "Mr. Lazy"; + + it("should add a unknown member", function() { + expect(state.getMember(userLazy)).toBeFalsy(); + state.setLazyLoadedMembers([{userId: userLazy}]); + const member = state.getMember(userLazy); + expect(member.userId).toEqual(userLazy); + expect(member.isLazyLoaded()).toEqual(true); + }); + + it("should emit newMember when adding a member", function() { + expect(state.getMember(userLazy)).toBeFalsy(); + let eventReceived = false; + state.once('RoomState.newMember', (_, __, member) => { + expect(member.userId).toEqual(userLazy); + eventReceived = true; + }); + state.setLazyLoadedMembers([{userId: userLazy}]); + expect(eventReceived).toEqual(true); + }); + + it("should not overwrite an existing member", function() { + state.setLazyLoadedMembers([{userId: userA, displayName: mrLazy}]); + const memberA = state.getMember(userA); + expect(memberA.name).toNotEqual(mrLazy); + expect(memberA.isLazyLoaded()).toEqual(false); + }); + + it("should update lazily loaded members if already present", function() { + state.setLazyLoadedMembers([{userId: userLazy}]); + state.setLazyLoadedMembers([{userId: userLazy, displayName: mrLazy}]); + expect(state.getMember(userLazy).name).toEqual(mrLazy); + }); + + it("should emit members when updating a member", function() { + state.setLazyLoadedMembers([{userId: userLazy}]); + let eventReceived = false; + state.once('RoomState.members', (_, __, member) => { + expect(member.userId).toEqual(userLazy); + eventReceived = true; + }); + state.setLazyLoadedMembers([{userId: userLazy, displayName: mrLazy}]); + expect(eventReceived).toEqual(true); + }); + + + it("should disambiguate name taking state event members into account", function() { + state.setLazyLoadedMembers([{userId: userLazy, displayName: userA}]); + const member = state.getMember(userLazy); + expect(member.name).toNotEqual(userA); //contain userA but not be equal + expect(member.name.indexOf(userA)).toNotEqual(-1); + }); + + }); + + describe("clone", function() { + it("should contain same information as original", function() { + // include LL members in copy + state.setLazyLoadedMembers([{userId: userLazy}]); + const copy = state.clone(); + const memberA = state.getMember(userA), + memberACopy = copy.getMember(userA), + memberB = state.getMember(userB), + memberBCopy = copy.getMember(userB), + memberLazy = state.getMember(userLazy), + memberLazyCopy = copy.getMember(userLazy); + // check individual members + expect(memberA.name).toEqual(memberACopy.name); + expect(memberA.isLazyLoaded()).toEqual(memberACopy.isLazyLoaded()); + expect(memberB.name).toEqual(memberBCopy.name); + expect(memberB.isLazyLoaded()).toEqual(memberBCopy.isLazyLoaded()); + expect(memberLazy.name).toEqual(memberLazyCopy.name); + expect(memberLazy.isLazyLoaded()).toEqual(memberLazyCopy.isLazyLoaded()); + // check member keys + expect(Object.keys(state.members)).toEqual(Object.keys(copy.members)); + // check join count + expect(state.getJoinedMemberCount()).toEqual(copy.getJoinedMemberCount()); + }); + + it("should return copy independent of original", function() { + + }); + }); + describe("setTypingEvent", function() { it("should call setTypingEvent on each RoomMember", function() { const typingEvent = utils.mkEvent({ diff --git a/src/models/room-member.js b/src/models/room-member.js index 3accbc29e..eda6ee271 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -112,14 +112,14 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) { * @param {Member} memberInfo a {userId, avatarUrl, displayName, membership} tuple * @param {RoomState} roomState the room state this member is part of, needed to disambiguate the display name */ -RoomMember.prototype.setAsLazyLoadedMember = function(memberInfo, roomState) { +RoomMember.prototype.setAsLazyLoadedMember = function(displayName, avatarUrl, membership, roomState) { if (this.events.member) { return; } - this.membership = memberInfo.membership; - this.name = calculateDisplayName(this.userId, memberInfo.displayName, roomState); - this.rawDisplayName = memberInfo.displayName || this.userId; - this._lazyLoadAvatarUrl = memberInfo.avatarUrl; + this.membership = membership; + this.name = calculateDisplayName(this.userId, displayName, roomState); + this.rawDisplayName = displayName || this.userId; + this._lazyLoadAvatarUrl = avatarUrl; this._isLazyLoaded = true; }; diff --git a/src/models/room-state.js b/src/models/room-state.js index 8805d53ac..fcf298250 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -155,7 +155,6 @@ RoomState.prototype.getStateEvents = function(eventType, stateKey) { */ RoomState.prototype.clone = function() { const copy = new RoomState(this.roomId); - //freeze and pass all state events to copy Object.values(this.events).forEach((eventsByStateKey) => { const eventsForType = Object.values(eventsByStateKey); copy.setStateEvents(eventsForType); @@ -164,7 +163,7 @@ RoomState.prototype.clone = function() { const lazyLoadedMembers = Object.values(this.members) .filter((member) => member.isLazyLoaded()); lazyLoadedMembers.forEach((m) => { - copy._setJoinedMember(m.userId, m.rawDisplayName, m.getMxcAvatarUrl()); + copy._setLazyLoadedMember(m.userId, m.rawDisplayName, m.getMxcAvatarUrl(), m.membership); }); return copy; }; @@ -279,18 +278,41 @@ RoomState.prototype._updateMember = function(member) { * @param {Member[]} members array of {userId, avatarUrl, displayName, membership} tuples */ RoomState.prototype.setLazyLoadedMembers = function(members) { - let newMembers = members.filter((m) => !this.members.hasOwnProperty(m.userId)); - newMembers.forEach((memberInfo) => { - const member = new RoomMember(this.roomId, memberInfo.userId); - // override the displayName and avatarUrl from the lazily loaded members - // as this is guaranteed to be the current state - member.setAsLazyLoadedMember(memberInfo, this); - _updateDisplayNameCache(this, member.userId, member.name); - this._updateMember(member); - this.emit('RoomState.newMember', {}, self, member); + members.forEach((m) => { + this._setLazyLoadedMember( + m.userId, + m.displayName, + m.avatarUrl, + m.membership + ); }); }; +/** + * Sets a single lazily loaded member, used by both setLazyLoadedMembers and clone + * @param {Member} members array of {userId, avatarUrl, displayName, membership} tuples + */ +RoomState.prototype._setLazyLoadedMember = function(userId, displayName, avatarUrl, membership) { + const preExistingMember = this.getMember(userId); + // don't overwrite existing state event members + if (preExistingMember && !preExistingMember.isLazyLoaded()) { + return; + } + const member = new RoomMember(this.roomId, userId); + member.setAsLazyLoadedMember(displayName, avatarUrl, membership, this); + _updateDisplayNameCache(this, member.userId, member.name); + this._updateMember(member); + + if (preExistingMember) { + this.emit("RoomState.members", {}, this, member); + } + else { + this.emit('RoomState.newMember', {}, this, member); + } +}; + + + /** * Set the current typing event for this room. * @param {MatrixEvent} event The typing event From ab1c0dabaed9059cf5987b3df71d0874094273ff Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 14:49:14 +0200 Subject: [PATCH 060/472] 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 --- src/models/room-member.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room-member.js b/src/models/room-member.js index eda6ee271..08181ef2a 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -302,7 +302,7 @@ RoomMember.prototype.getMxcAvatarUrl = function() { }; function calculateDisplayName(selfUserId, displayName, roomState) { - if (!displayName) { + if (!displayName || displayName === selfUserId) { return selfUserId; } From 198d2c780dda324eb74e76cb790a7f009546b6c9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 15:21:04 +0200 Subject: [PATCH 061/472] test that modifications to clone'd() room state dont affect the old --- spec/unit/room-state.spec.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/spec/unit/room-state.spec.js b/spec/unit/room-state.spec.js index 4bd085908..365b25128 100644 --- a/spec/unit/room-state.spec.js +++ b/spec/unit/room-state.spec.js @@ -12,6 +12,8 @@ describe("RoomState", function() { const userA = "@alice:bar"; const userB = "@bob:bar"; const userLazy = "@lazy:bar"; + const mrLazy = "Mr. Lazy"; + let state; beforeEach(function() { @@ -265,8 +267,6 @@ describe("RoomState", function() { }); describe("setLazyLoadedMembers", function() { - const mrLazy = "Mr. Lazy"; - it("should add a unknown member", function() { expect(state.getMember(userLazy)).toBeFalsy(); state.setLazyLoadedMembers([{userId: userLazy}]); @@ -345,7 +345,22 @@ describe("RoomState", function() { }); it("should return copy independent of original", function() { + // include LL members in copy + state.setLazyLoadedMembers([{userId: userLazy, membership: "join"}]); + const copy = state.clone(); + const memberLazyCopy = copy.getMember(userLazy); + memberLazyCopy.setAsLazyLoadedMember(mrLazy, null, "join"); + expect(state.getMember(userLazy).name).toEqual(userLazy); + expect(memberLazyCopy.name).toEqual(mrLazy); + expect(state.getJoinedMemberCount()).toEqual(3); + + copy.setStateEvents([utils.mkMembership({ // userA leaves + event: true, mship: "leave", user: userA, room: roomId, + })]); + + expect(state.getJoinedMemberCount()).toEqual(3); + expect(copy.getJoinedMemberCount()).toEqual(2); }); }); From 9f08bfaa6f9a4d4c13e43d4c9618f3cd6af9dc29 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 17:09:38 +0200 Subject: [PATCH 062/472] room lazy loading tests + fix --- spec/unit/room.spec.js | 60 ++++++++++++++++++++++++++++++++++++++++++ src/models/room.js | 3 +++ 2 files changed, 63 insertions(+) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 793a6b5e7..6a4a8f598 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1272,4 +1272,64 @@ describe("Room", function() { expect(callCount).toEqual(1); }); }); + + describe("setLazyLoadedMembers", function() { + it("should apply member info in promise", async function() { + const room = new Room(roomId); + expect(room.membersNeedLoading()).toEqual(true); + const infoA = {userId: userA, membership: "invite"}; + const infoB = {userId: userB, membership: "join"}; + const promise = room.setLazyLoadedMembers(Promise.resolve([infoA, infoB])); + await promise; + expect(room.membersNeedLoading()).toEqual(false); + const memberA = room.getMember(userA); + const memberB = room.getMember(userB); + expect(memberA.membership).toEqual("invite"); + expect(memberA.isLazyLoaded()).toEqual(true); + expect(memberB.membership).toEqual("join"); + expect(memberB.isLazyLoaded()).toEqual(true); + }); + + it("should revert needs loading on error", async function() { + const room = new Room(roomId); + let hasThrown = false; + try { + await room.setLazyLoadedMembers(Promise.reject(new Error("bugger"))); + } + catch(err) { + hasThrown = true; + } + expect(hasThrown).toEqual(true); + expect(room.membersNeedLoading()).toEqual(true); + }); + + it("should revert needs loading on error", async function() { + const room = new Room(roomId); + let hasThrown = false; + try { + await room.setLazyLoadedMembers(Promise.reject(new Error("bugger"))); + } + catch(err) { + hasThrown = true; + } + expect(hasThrown).toEqual(true); + expect(room.membersNeedLoading()).toEqual(true); + }); + + it("second call (also in immediate succession) should be ignored", async function() { + const room = new Room(roomId); + const promise1 = room.setLazyLoadedMembers(Promise.resolve([ + {userId: userA, membership: "join"}, + {userId: userB, membership: "join"} + ])); + const promise2 = room.setLazyLoadedMembers(Promise.resolve([ + {userId: userC, membership: "join"} + ])); + await Promise.all([promise1, promise2]); + expect(room.getMember(userA)).toBeTruthy(); + expect(room.getMember(userB)).toBeTruthy(); + expect(room.getMember(userC)).toBeFalsy(); + }); + + }); }); diff --git a/src/models/room.js b/src/models/room.js index a5cade4e6..ffda156cc 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -229,6 +229,9 @@ Room.prototype.membersNeedLoading = function() { * @param {Promise} membersPromise promise with array of {userId, avatarUrl, displayName, membership} tuples */ Room.prototype.setLazyLoadedMembers = async function(membersPromise) { + if (!this._membersNeedLoading) { + return; + } this._membersNeedLoading = false; let members = null; try { From df758b31b79ac6d8b10ba95a47a798a3be7c53c1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 20 Jul 2018 17:22:22 +0200 Subject: [PATCH 063/472] fix lint --- spec/unit/room-member.spec.js | 14 +++++++++----- spec/unit/room-state.spec.js | 17 ++++++++--------- spec/unit/room.spec.js | 24 +++++------------------- src/models/room-member.js | 7 +++++-- src/models/room-state.js | 22 +++++++++++++--------- 5 files changed, 40 insertions(+), 44 deletions(-) diff --git a/spec/unit/room-member.spec.js b/spec/unit/room-member.spec.js index 7fc7af191..8b6e5cec1 100644 --- a/spec/unit/room-member.spec.js +++ b/spec/unit/room-member.spec.js @@ -198,13 +198,17 @@ describe("RoomMember", function() { const displayName = "Mr. Lazy"; const memberInfo = { avatarUrl: "mxc://lazy/loaded", - displayName: displayName , - membership: "join" + displayName: displayName, + membership: "join", }; it("should allow precedence of state events", function() { const member = new RoomMember(roomId, lazyUserId); - member.setAsLazyLoadedMember(memberInfo.displayName, memberInfo.avatarUrl, memberInfo.membership); + member.setAsLazyLoadedMember( + memberInfo.displayName, + memberInfo.avatarUrl, + memberInfo.membership); + let url = member.getAvatarUrl(hsUrl); expect(url.indexOf("lazy/loaded")).toNotEqual(-1); expect(member.name).toEqual(displayName); @@ -224,7 +228,7 @@ describe("RoomMember", function() { })); url = member.getAvatarUrl(hsUrl); - + // check that the member can't be set as lazy loaded anymore // once it has a member event for(let i = 0; i < 2; ++i) { @@ -232,7 +236,7 @@ describe("RoomMember", function() { expect(member.name).toEqual("Mr. State"); expect(member.isLazyLoaded()).toEqual(false); member.setAsLazyLoadedMember(memberInfo); - } + } }); }); diff --git a/spec/unit/room-state.spec.js b/spec/unit/room-state.spec.js index 365b25128..f99077c7a 100644 --- a/spec/unit/room-state.spec.js +++ b/spec/unit/room-state.spec.js @@ -309,15 +309,14 @@ describe("RoomState", function() { state.setLazyLoadedMembers([{userId: userLazy, displayName: mrLazy}]); expect(eventReceived).toEqual(true); }); - - it("should disambiguate name taking state event members into account", function() { + it("should disambiguate name taking state event members into account", + function() { state.setLazyLoadedMembers([{userId: userLazy, displayName: userA}]); const member = state.getMember(userLazy); expect(member.name).toNotEqual(userA); //contain userA but not be equal expect(member.name.indexOf(userA)).toNotEqual(-1); }); - }); describe("clone", function() { @@ -325,12 +324,12 @@ describe("RoomState", function() { // include LL members in copy state.setLazyLoadedMembers([{userId: userLazy}]); const copy = state.clone(); - const memberA = state.getMember(userA), - memberACopy = copy.getMember(userA), - memberB = state.getMember(userB), - memberBCopy = copy.getMember(userB), - memberLazy = state.getMember(userLazy), - memberLazyCopy = copy.getMember(userLazy); + const memberA = state.getMember(userA); + const memberACopy = copy.getMember(userA); + const memberB = state.getMember(userB); + const memberBCopy = copy.getMember(userB); + const memberLazy = state.getMember(userLazy); + const memberLazyCopy = copy.getMember(userLazy); // check individual members expect(memberA.name).toEqual(memberACopy.name); expect(memberA.isLazyLoaded()).toEqual(memberACopy.isLazyLoaded()); diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 6a4a8f598..5af4005a2 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1295,41 +1295,27 @@ describe("Room", function() { let hasThrown = false; try { await room.setLazyLoadedMembers(Promise.reject(new Error("bugger"))); - } - catch(err) { + } catch(err) { hasThrown = true; } expect(hasThrown).toEqual(true); expect(room.membersNeedLoading()).toEqual(true); }); - it("should revert needs loading on error", async function() { - const room = new Room(roomId); - let hasThrown = false; - try { - await room.setLazyLoadedMembers(Promise.reject(new Error("bugger"))); - } - catch(err) { - hasThrown = true; - } - expect(hasThrown).toEqual(true); - expect(room.membersNeedLoading()).toEqual(true); - }); - - it("second call (also in immediate succession) should be ignored", async function() { + it("second call (also in immediate succession) should be ignored", + async function() { const room = new Room(roomId); const promise1 = room.setLazyLoadedMembers(Promise.resolve([ {userId: userA, membership: "join"}, - {userId: userB, membership: "join"} + {userId: userB, membership: "join"}, ])); const promise2 = room.setLazyLoadedMembers(Promise.resolve([ - {userId: userC, membership: "join"} + {userId: userC, membership: "join"}, ])); await Promise.all([promise1, promise2]); expect(room.getMember(userA)).toBeTruthy(); expect(room.getMember(userB)).toBeTruthy(); expect(room.getMember(userC)).toBeFalsy(); }); - }); }); diff --git a/src/models/room-member.js b/src/models/room-member.js index 08181ef2a..1d4785a6d 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -109,10 +109,13 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) { /** * Update this room member from a lazily loaded member - * @param {Member} memberInfo a {userId, avatarUrl, displayName, membership} tuple + * @param {string} displayName display name for lazy loaded member + * @param {string} avatarUrl avatar url for lazy loaded member + * @param {string} membership membership (join|invite|...) state for lazy loaded member * @param {RoomState} roomState the room state this member is part of, needed to disambiguate the display name */ -RoomMember.prototype.setAsLazyLoadedMember = function(displayName, avatarUrl, membership, roomState) { +RoomMember.prototype.setAsLazyLoadedMember = +function(displayName, avatarUrl, membership, roomState) { if (this.events.member) { return; } diff --git a/src/models/room-state.js b/src/models/room-state.js index fcf298250..96ed6d797 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -163,7 +163,11 @@ RoomState.prototype.clone = function() { const lazyLoadedMembers = Object.values(this.members) .filter((member) => member.isLazyLoaded()); lazyLoadedMembers.forEach((m) => { - copy._setLazyLoadedMember(m.userId, m.rawDisplayName, m.getMxcAvatarUrl(), m.membership); + copy._setLazyLoadedMember( + m.userId, + m.rawDisplayName, + m.getMxcAvatarUrl(), + m.membership); }); return copy; }; @@ -283,16 +287,19 @@ RoomState.prototype.setLazyLoadedMembers = function(members) { m.userId, m.displayName, m.avatarUrl, - m.membership - ); + m.membership); }); }; /** * Sets a single lazily loaded member, used by both setLazyLoadedMembers and clone - * @param {Member} members array of {userId, avatarUrl, displayName, membership} tuples + * @param {string} userId user id for lazy loaded member + * @param {string} displayName display name for lazy loaded member + * @param {string} avatarUrl avatar url for lazy loaded member + * @param {string} membership membership (join|invite|...) state for lazy loaded member */ -RoomState.prototype._setLazyLoadedMember = function(userId, displayName, avatarUrl, membership) { +RoomState.prototype._setLazyLoadedMember = +function(userId, displayName, avatarUrl, membership) { const preExistingMember = this.getMember(userId); // don't overwrite existing state event members if (preExistingMember && !preExistingMember.isLazyLoaded()) { @@ -305,14 +312,11 @@ RoomState.prototype._setLazyLoadedMember = function(userId, displayName, avatarU if (preExistingMember) { this.emit("RoomState.members", {}, this, member); - } - else { + } else { this.emit('RoomState.newMember', {}, this, member); } }; - - /** * Set the current typing event for this room. * @param {MatrixEvent} event The typing event From 62333b3e2c01c84257469cb81f6c9a2780e51c91 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 24 Jul 2018 18:55:39 +0200 Subject: [PATCH 064/472] 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. --- src/base-apis.js | 22 ++++- src/client.js | 34 ++----- src/models/room-member.js | 70 +++++++++------ src/models/room-state.js | 184 ++++++++++++++++++++++++++++++-------- src/models/room.js | 38 ++++---- 5 files changed, 235 insertions(+), 113 deletions(-) diff --git a/src/base-apis.js b/src/base-apis.js index 624a81681..a85ed9bc6 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -419,12 +419,30 @@ MatrixBaseApis.prototype.roomState = function(roomId, callback) { /** * @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.joinedMembers = function(roomId, callback) { - const path = utils.encodeUri("/rooms/$roomId/joined_members", {$roomId: roomId}); +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); }; diff --git a/src/client.js b/src/client.js index 63cf770cd..d464c3fc1 100644 --- a/src/client.js +++ b/src/client.js @@ -764,36 +764,14 @@ MatrixClient.prototype.getRoom = function(roomId) { */ MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { const room = this.getRoom(roomId); - if (!room || !room.membersNeedLoading()) { + if (!room || !room.needsOutOfBandMembers()) { return; } - // XXX: we should make sure that the members we get back represent the - // room state at a given point in time. The plan is to do this by - // passing the current next_batch sync token to the endpoint we use - // to fetch the members. For now, this is a prototype that uses - // the /joined_members api, which only tells us about the joined members - // (not invites for example) and does not support this synchronization. - // So there is a race condition here between the current /sync call - // and the /joined_members call: if the have conflicting information, which one - // represents the most recent state? - // - // Addressing this race condition and the fact that this only tells us about - // joined members is a prerequisite for taking this out of the prototype stage and - // enabling the feature flag (feature_lazyloading) that - // the call to this method is behind. - const joinedMembersPromise = this.joinedMembers(roomId); - const membersPromise = joinedMembersPromise.then((profiles) => { - return Object.entries(profiles.joined).map(([userId, profile]) => { - return { - userId: userId, - avatarUrl: profile.avatar_url, - displayName: profile.display_name, - membership: "join", // as we need to support invitees as well - // in the future, already include but hardcode it - }; - }); - }); - await room.setLazyLoadedMembers(membersPromise); + + const lastEventId = room.getLastEventId(); + const responsePromise = this.members(roomId, "join", "leave", lastEventId); + const eventsPromise = responsePromise.then((response) => response.chunk); + await room.loadOutOfBandMembers(eventsPromise); }; /** diff --git a/src/models/room-member.js b/src/models/room-member.js index 1d4785a6d..7abcbb199 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -58,14 +58,50 @@ function RoomMember(roomId, userId) { this.events = { member: null, }; - this._lazyLoadAvatarUrl = null; - this._isLazyLoaded = false; + this._isOutOfBand = false; + this._supersedesOutOfBand = false; this._updateModifiedTime(); } utils.inherits(RoomMember, EventEmitter); -RoomMember.prototype.isLazyLoaded = function() { - return this._isLazyLoaded; +/** + * Mark the member as coming from a channel that is not sync + */ +RoomMember.prototype.markOutOfBand = function() { + this._isOutOfBand = true; +}; + +/** + * @returns {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; +}; + +/** + * Does the member supersede an incoming out-of-band + * member? If so the out-of-band member should be ignored. + */ +RoomMember.prototype.supersedesOutOfBand = function() { + this._supersedesOutOfBand; +}; + +/** + * Mark the member as superseding the future incoming + * out-of-band members. + */ +RoomMember.prototype.markSupersedesOutOfBand = function() { + this._supersedesOutOfBand = true; +}; + +/** + * Clear the member superseding the future incoming + * out-of-band members, as loading finished or failed. + */ +RoomMember.prototype.clearSupersedesOutOfBand = function() { + this._supersedesOutOfBand = false; }; /** @@ -82,8 +118,7 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) { return; } - this._lazyLoadAvatarUrl = null; - this._isLazyLoaded = false; + this._isOutOfBand = false; this.events.member = event; @@ -107,25 +142,6 @@ RoomMember.prototype.setMembershipEvent = function(event, roomState) { } }; -/** - * Update this room member from a lazily loaded member - * @param {string} displayName display name for lazy loaded member - * @param {string} avatarUrl avatar url for lazy loaded member - * @param {string} membership membership (join|invite|...) state for lazy loaded member - * @param {RoomState} roomState the room state this member is part of, needed to disambiguate the display name - */ -RoomMember.prototype.setAsLazyLoadedMember = -function(displayName, avatarUrl, membership, roomState) { - if (this.events.member) { - return; - } - this.membership = membership; - this.name = calculateDisplayName(this.userId, displayName, roomState); - this.rawDisplayName = displayName || this.userId; - this._lazyLoadAvatarUrl = avatarUrl; - this._isLazyLoaded = true; -}; - /** * Update this room member's power level event. May fire * "RoomMember.powerLevel" if this event updates this member's power levels. @@ -294,9 +310,7 @@ RoomMember.prototype.getAvatarUrl = * @return {string} the mxc avatar url */ RoomMember.prototype.getMxcAvatarUrl = function() { - if (this._lazyLoadAvatarUrl) { - return this._lazyLoadAvatarUrl; - } else if(this.events.member) { + if(this.events.member) { return this.events.member.getContent().avatar_url; } else if(this.user) { return this.user.avatarUrl; diff --git a/src/models/room-state.js b/src/models/room-state.js index 96ed6d797..4fe608bbf 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -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.} members The room member dictionary, keyed * on the user's ID. * @prop {Object.>} 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 @@ -70,6 +79,12 @@ function RoomState(roomId) { this._userIdsToDisplayNames = {}; this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite this._joinedMemberCount = null; // cache of the number of joined members + if (!oobMemberFlags) { + oobMemberFlags = { + status: OOB_STATUS_NOTSTARTED, + }; + } + this._oobMemberFlags = oobMemberFlags; } utils.inherits(RoomState, EventEmitter); @@ -154,21 +169,45 @@ RoomState.prototype.getStateEvents = function(eventType, stateKey) { * @return {RoomState} the copy of the room state */ RoomState.prototype.clone = function() { - const copy = new RoomState(this.roomId); + 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 and + // copy the superseding flag from the current state + 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); }); - // clone lazily loaded members - const lazyLoadedMembers = Object.values(this.members) - .filter((member) => member.isLazyLoaded()); - lazyLoadedMembers.forEach((m) => { - copy._setLazyLoadedMember( - m.userId, - m.rawDisplayName, - m.getMxcAvatarUrl(), - m.membership); - }); + + // Ugly hack: see above + this._oobMemberFlags.status = status; + + // 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(); + } + }); + } else if (this._oobMemberFlags.status == OOB_STATUS_INPROGRESS) { + // copy markSupersedesOutOfBand flags + this.getMembers().forEach((member) => { + if (member.supersedesOutOfBand()) { + const copyMember = copy.getMember(member.userId); + copyMember.markSupersedesOutOfBand(); + } + }); + } + return copy; }; @@ -195,10 +234,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, @@ -248,6 +284,13 @@ RoomState.prototype.setStateEvents = function(stateEvents) { } member.setMembershipEvent(event, self); + + // if out of band members are loading, + // mark the member as more recent + if (self._oobMemberFlags.status == OOB_STATUS_INPROGRESS) { + member.markSupersedesOutOfBand(); + } + self._updateMember(member); self.emit("RoomState.members", event, self, member); } else if (event.getType() === "m.room.power_levels") { @@ -263,6 +306,13 @@ RoomState.prototype.setStateEvents = function(stateEvents) { }); }; +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", ""); @@ -278,39 +328,95 @@ RoomState.prototype._updateMember = function(member) { }; /** - * Sets the lazily loaded members. - * @param {Member[]} members array of {userId, avatarUrl, displayName, membership} tuples + * 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.setLazyLoadedMembers = function(members) { - members.forEach((m) => { - this._setLazyLoadedMember( - m.userId, - m.displayName, - m.avatarUrl, - m.membership); - }); +RoomState.prototype.needsOutOfBandMembers = function() { + return this._oobMemberFlags.status === OOB_STATUS_NOTSTARTED; }; /** - * Sets a single lazily loaded member, used by both setLazyLoadedMembers and clone - * @param {string} userId user id for lazy loaded member - * @param {string} displayName display name for lazy loaded member - * @param {string} avatarUrl avatar url for lazy loaded member - * @param {string} membership membership (join|invite|...) state for lazy loaded member + * 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._setLazyLoadedMember = -function(userId, displayName, avatarUrl, membership) { - const preExistingMember = this.getMember(userId); - // don't overwrite existing state event members - if (preExistingMember && !preExistingMember.isLazyLoaded()) { +RoomState.prototype.markOutOfBandMembersStarted = function() { + if (this._oobMemberFlags.status !== OOB_STATUS_NOTSTARTED) { return; } - const member = new RoomMember(this.roomId, userId); - member.setAsLazyLoadedMember(displayName, avatarUrl, membership, this); + 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; + } + // the request failed, there is nothing to supersede + // in case of a retry, these event would not supersede the + // retry anymore. + this.getMembers().forEach((m) => { + m.clearSupersedesOutOfBand(); + }); + 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) { + if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) { + return; + } + 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); + if (existingMember) { + const existingMemberEvent = existingMember.events.member; + // ignore out of band members with events we are + // already aware of. + if (existingMemberEvent.getId() === stateEvent.getId()) { + return; + } + // this member was updated since we started + // loading the out of band members. + // Ignore the out of band member and clear + // the "supersedes" flag as the out of members are now loaded + if (existingMember.supersedesOutOfBand()) { + existingMember.clearSupersedesOutOfBand(); + return; + } + } + + const member = + existingMember ? existingMember : new RoomMember(this.roomId, userId); + member.setMembershipEvent(stateEvent); + // needed to know which members need to be stored seperately + // as the 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); - if (preExistingMember) { + if (existingMember) { this.emit("RoomState.members", {}, this, member); } else { this.emit('RoomState.newMember', {}, this, member); diff --git a/src/models/room.js b/src/models/room.js index ffda156cc..f09fc659d 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -216,32 +216,38 @@ Room.prototype.getLiveTimeline = function() { return this.getUnfilteredTimelineSet().getLiveTimeline(); }; -/** - * Get the lazy loading state, whether loading is needed or not. - * @return {bool} whether or not the members of this room need to be loaded - */ -Room.prototype.membersNeedLoading = function() { - return this._membersNeedLoading; +Room.prototype.getLastEventId = function() { + const liveEvents = this.getLiveTimeline().getEvents(); + return liveEvents.length ? liveEvents[liveEvents.length - 1].getId() : undefined; }; /** - * Sets the lazily loaded members from the result of calling /joined_members - * @param {Promise} membersPromise promise with array of {userId, avatarUrl, displayName, membership} tuples + * 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 */ -Room.prototype.setLazyLoadedMembers = async function(membersPromise) { - if (!this._membersNeedLoading) { +Room.prototype.needsOutOfBandMembers = function() { + return this.currentState.needsOutOfBandMembers(); +}; + +/** + * Loads the out-of-band members from the promise passed in + * @param {Promise} eventsPromise promise with array with state events + */ +Room.prototype.loadOutOfBandMembers = async function(eventsPromise) { + if (!this.membersNeedLoading()) { return; } - this._membersNeedLoading = false; - let members = null; + this.currentState.markOutOfBandMembersStarted(); + let eventPojos = null; try { - members = await membersPromise; + eventPojos = await eventsPromise; } catch (err) { - this._membersNeedLoading = true; + this.currentState.markOutOfBandMembersFailed(); throw err; //rethrow so calling code is aware operation failed } - this.currentState.setLazyLoadedMembers(members); - this.emit('Room', this); + const events = eventPojos.map(this.client.getEventMapper()); + this.currentState.setOutOfBandMembers(events); }; /** From 48c3dcc08ac4d44bdf17a28b58189ee21fee37f5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Jul 2018 11:23:58 +0200 Subject: [PATCH 065/472] fix lint & fix and add tests --- spec/unit/room-member.spec.js | 65 +++++--------- spec/unit/room-state.spec.js | 162 +++++++++++++++++++++++----------- spec/unit/room.spec.js | 38 +------- src/models/room-member.js | 5 +- 4 files changed, 139 insertions(+), 131 deletions(-) diff --git a/spec/unit/room-member.spec.js b/spec/unit/room-member.spec.js index 8b6e5cec1..5212282b1 100644 --- a/spec/unit/room-member.spec.js +++ b/spec/unit/room-member.spec.js @@ -192,51 +192,28 @@ describe("RoomMember", function() { }); }); - describe("setAsLazyLoadedMember", function() { - const hsUrl = "https://my.home.server"; - const lazyUserId = "@lazy:bar"; - const displayName = "Mr. Lazy"; - const memberInfo = { - avatarUrl: "mxc://lazy/loaded", - displayName: displayName, - membership: "join", - }; - it("should allow precedence of state events", - function() { - const member = new RoomMember(roomId, lazyUserId); - member.setAsLazyLoadedMember( - memberInfo.displayName, - memberInfo.avatarUrl, - memberInfo.membership); + 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); + }); + }); - let url = member.getAvatarUrl(hsUrl); - expect(url.indexOf("lazy/loaded")).toNotEqual(-1); - expect(member.name).toEqual(displayName); - expect(member.isLazyLoaded()).toEqual(true); - - member.setMembershipEvent(utils.mkEvent({ - event: true, - type: "m.room.member", - skey: lazyUserId, - room: roomId, - user: lazyUserId, - content: { - displayname: "Mr. State", - membership: "join", - avatar_url: "mxc://flibble/wibble", - }, - })); - - url = member.getAvatarUrl(hsUrl); - - // check that the member can't be set as lazy loaded anymore - // once it has a member event - for(let i = 0; i < 2; ++i) { - expect(url.indexOf("flibble/wibble")).toNotEqual(-1); - expect(member.name).toEqual("Mr. State"); - expect(member.isLazyLoaded()).toEqual(false); - member.setAsLazyLoadedMember(memberInfo); - } + describe("supersedesOutOfBand", function() { + it("should be set by markSupersedesOutOfBand", function() { + const member = new RoomMember(); + expect(member.supersedesOutOfBand()).toEqual(false); + member.markSupersedesOutOfBand(); + expect(member.supersedesOutOfBand()).toEqual(true); + }); + it("should be cleared by clearSupersedesOutOfBand", function() { + const member = new RoomMember(); + member.markSupersedesOutOfBand(); + expect(member.supersedesOutOfBand()).toEqual(true); + member.clearSupersedesOutOfBand(); + expect(member.supersedesOutOfBand()).toEqual(false); }); }); diff --git a/spec/unit/room-state.spec.js b/spec/unit/room-state.spec.js index f99077c7a..626232783 100644 --- a/spec/unit/room-state.spec.js +++ b/spec/unit/room-state.spec.js @@ -12,7 +12,6 @@ describe("RoomState", function() { const userA = "@alice:bar"; const userB = "@bob:bar"; const userLazy = "@lazy:bar"; - const mrLazy = "Mr. Lazy"; let state; @@ -266,100 +265,163 @@ describe("RoomState", function() { }); }); - describe("setLazyLoadedMembers", function() { - it("should add a unknown member", function() { - expect(state.getMember(userLazy)).toBeFalsy(); - state.setLazyLoadedMembers([{userId: userLazy}]); + 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.isLazyLoaded()).toEqual(true); + 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() { - expect(state.getMember(userLazy)).toBeFalsy(); + 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.setLazyLoadedMembers([{userId: userLazy}]); + state.markOutOfBandMembersStarted(); + state.setOutOfBandMembers([oobMemberEvent]); expect(eventReceived).toEqual(true); }); - it("should not overwrite an existing member", function() { - state.setLazyLoadedMembers([{userId: userA, displayName: mrLazy}]); + it("should 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.name).toNotEqual(mrLazy); - expect(memberA.isLazyLoaded()).toEqual(false); + expect(memberA.events.member.getId()).toEqual(oobMemberEvent.getId()); + expect(memberA.isOutOfBand()).toEqual(true); }); - it("should update lazily loaded members if already present", function() { - state.setLazyLoadedMembers([{userId: userLazy}]); - state.setLazyLoadedMembers([{userId: userLazy, displayName: mrLazy}]); - expect(state.getMember(userLazy).name).toEqual(mrLazy); + it("should allow later state events to overwrite", function() { + const oobMemberEvent = utils.mkMembership({ + user: userA, mship: "join", room: roomId, event: true, + }); + const memberEvent = utils.mkMembership({ + user: userA, mship: "join", room: roomId, event: true, + }); + + state.markOutOfBandMembersStarted(); + state.setOutOfBandMembers([oobMemberEvent]); + state.setStateEvents([memberEvent]); + + const memberA = state.getMember(userA); + expect(memberA.events.member.getId()).toEqual(memberEvent.getId()); + expect(memberA.isOutOfBand()).toEqual(false); }); it("should emit members when updating a member", function() { - state.setLazyLoadedMembers([{userId: userLazy}]); + const oobMemberEvent = utils.mkMembership({ + user: userA, mship: "join", room: roomId, event: true, + }); let eventReceived = false; state.once('RoomState.members', (_, __, member) => { - expect(member.userId).toEqual(userLazy); + expect(member.userId).toEqual(userA); eventReceived = true; }); - state.setLazyLoadedMembers([{userId: userLazy, displayName: mrLazy}]); + + state.markOutOfBandMembersStarted(); + state.setOutOfBandMembers([oobMemberEvent]); expect(eventReceived).toEqual(true); }); - it("should disambiguate name taking state event members into account", + + it("should not overwrite members updated since starting loading oob", function() { - state.setLazyLoadedMembers([{userId: userLazy, displayName: userA}]); - const member = state.getMember(userLazy); - expect(member.name).toNotEqual(userA); //contain userA but not be equal - expect(member.name.indexOf(userA)).toNotEqual(-1); + const oobMemberEvent = utils.mkMembership({ + user: userA, mship: "join", room: roomId, event: true, + }); + + const existingMemberEvent = utils.mkMembership({ + user: userA, mship: "join", room: roomId, event: true, + }); + + state.markOutOfBandMembersStarted(); + state.setStateEvents([existingMemberEvent]); + expect(state.getMember(userA).supersedesOutOfBand()).toEqual(true); + state.setOutOfBandMembers([oobMemberEvent]); + + const memberA = state.getMember(userA); + expect(memberA.events.member.getId()).toEqual(existingMemberEvent.getId()); + expect(memberA.isOutOfBand()).toEqual(false); + expect(memberA.supersedesOutOfBand()).toEqual(false); }); }); describe("clone", function() { it("should contain same information as original", function() { - // include LL members in copy - state.setLazyLoadedMembers([{userId: userLazy}]); + // include OOB members in copy + state.markOutOfBandMembersStarted(); + state.setOutOfBandMembers([utils.mkMembership({ + user: userLazy, mship: "join", room: roomId, event: true, + })]); const copy = state.clone(); - const memberA = state.getMember(userA); - const memberACopy = copy.getMember(userA); - const memberB = state.getMember(userB); - const memberBCopy = copy.getMember(userB); - const memberLazy = state.getMember(userLazy); - const memberLazyCopy = copy.getMember(userLazy); // check individual members - expect(memberA.name).toEqual(memberACopy.name); - expect(memberA.isLazyLoaded()).toEqual(memberACopy.isLazyLoaded()); - expect(memberB.name).toEqual(memberBCopy.name); - expect(memberB.isLazyLoaded()).toEqual(memberBCopy.isLazyLoaded()); - expect(memberLazy.name).toEqual(memberLazyCopy.name); - expect(memberLazy.isLazyLoaded()).toEqual(memberLazyCopy.isLazyLoaded()); + [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 return copy independent of original", function() { - // include LL members in copy - state.setLazyLoadedMembers([{userId: userLazy, membership: "join"}]); + it("should copy supersedes flag when OOB loading is progress", + function() { + // include OOB members in copy + state.markOutOfBandMembersStarted(); + state.setStateEvents([utils.mkMembership({ + user: userA, mship: "join", room: roomId, event: true, + })]); const copy = state.clone(); - const memberLazyCopy = copy.getMember(userLazy); - memberLazyCopy.setAsLazyLoadedMember(mrLazy, null, "join"); - expect(state.getMember(userLazy).name).toEqual(userLazy); - expect(memberLazyCopy.name).toEqual(mrLazy); + const memberA = state.getMember(userA); + const memberACopy = copy.getMember(userA); + expect(memberA.supersedesOutOfBand()).toEqual(true); + expect(memberACopy.supersedesOutOfBand()).toEqual(true); + }); - expect(state.getJoinedMemberCount()).toEqual(3); + 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(); + }); - copy.setStateEvents([utils.mkMembership({ // userA leaves - event: true, mship: "leave", user: userA, room: roomId, + 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.getJoinedMemberCount()).toEqual(3); - expect(copy.getJoinedMemberCount()).toEqual(2); + expect(state.getMember(userLazy)).toBeFalsy(); + expect(state.getJoinedMemberCount()).toEqual(2); + expect(copy.getJoinedMemberCount()).toEqual(3); }); }); diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 5af4005a2..61b606f6d 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1273,49 +1273,17 @@ describe("Room", function() { }); }); - describe("setLazyLoadedMembers", function() { - it("should apply member info in promise", async function() { - const room = new Room(roomId); - expect(room.membersNeedLoading()).toEqual(true); - const infoA = {userId: userA, membership: "invite"}; - const infoB = {userId: userB, membership: "join"}; - const promise = room.setLazyLoadedMembers(Promise.resolve([infoA, infoB])); - await promise; - expect(room.membersNeedLoading()).toEqual(false); - const memberA = room.getMember(userA); - const memberB = room.getMember(userB); - expect(memberA.membership).toEqual("invite"); - expect(memberA.isLazyLoaded()).toEqual(true); - expect(memberB.membership).toEqual("join"); - expect(memberB.isLazyLoaded()).toEqual(true); - }); - + describe("loadOutOfBandMembers", function() { it("should revert needs loading on error", async function() { const room = new Room(roomId); let hasThrown = false; try { - await room.setLazyLoadedMembers(Promise.reject(new Error("bugger"))); + await room.loadOutOfBandMembers(Promise.reject(new Error("bugger"))); } catch(err) { hasThrown = true; } expect(hasThrown).toEqual(true); - expect(room.membersNeedLoading()).toEqual(true); - }); - - it("second call (also in immediate succession) should be ignored", - async function() { - const room = new Room(roomId); - const promise1 = room.setLazyLoadedMembers(Promise.resolve([ - {userId: userA, membership: "join"}, - {userId: userB, membership: "join"}, - ])); - const promise2 = room.setLazyLoadedMembers(Promise.resolve([ - {userId: userC, membership: "join"}, - ])); - await Promise.all([promise1, promise2]); - expect(room.getMember(userA)).toBeTruthy(); - expect(room.getMember(userB)).toBeTruthy(); - expect(room.getMember(userC)).toBeFalsy(); + expect(room.needsOutOfBandMembers()).toEqual(true); }); }); }); diff --git a/src/models/room-member.js b/src/models/room-member.js index 7abcbb199..71504d5b6 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -72,7 +72,7 @@ RoomMember.prototype.markOutOfBand = function() { }; /** - * @returns {bool} does the member come from a channel that is not sync? + * @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. */ @@ -83,9 +83,10 @@ RoomMember.prototype.isOutOfBand = function() { /** * Does the member supersede an incoming out-of-band * member? If so the out-of-band member should be ignored. + * @return {bool} */ RoomMember.prototype.supersedesOutOfBand = function() { - this._supersedesOutOfBand; + return this._supersedesOutOfBand; }; /** From 5fcf9481b339248aec182e16963313073f38289a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Jul 2018 12:35:34 +0200 Subject: [PATCH 066/472] fix room not having access to event mapper + tests --- spec/unit/room.spec.js | 29 +++++++++++++++++++++++++++++ src/client.js | 4 +++- src/models/room.js | 9 ++++----- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 61b606f6d..85d77269e 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1274,6 +1274,35 @@ describe("Room", function() { }); describe("loadOutOfBandMembers", function() { + const memberEvent = utils.mkMembership({ + user: "@user_a:bar", mship: "join", + room: roomId, event: true, name: "User A", + }); + + it("should apply member events", async function() { + const room = new Room(roomId); + await room.loadOutOfBandMembers(Promise.resolve([memberEvent])); + const memberA = room.getMember("@user_a:bar"); + expect(memberA.name).toEqual("User A"); + }); + + it("should apply first call, not first resolved promise", async function() { + const memberEvent2 = utils.mkMembership({ + user: "@user_a:bar", mship: "join", + room: roomId, event: true, name: "Ms A", + }); + const room = new Room(roomId); + + const promise2 = Promise.resolve([memberEvent2]) + const promise1 = promise2.then(() => [memberEvent]); + + await room.loadOutOfBandMembers(promise1); + await room.loadOutOfBandMembers(promise2); + + const memberA = room.getMember("@user_a:bar"); + expect(memberA.name).toEqual("User A"); + }); + it("should revert needs loading on error", async function() { const room = new Room(roomId); let hasThrown = false; diff --git a/src/client.js b/src/client.js index d464c3fc1..0560ff3d1 100644 --- a/src/client.js +++ b/src/client.js @@ -770,7 +770,9 @@ MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { const lastEventId = room.getLastEventId(); const responsePromise = this.members(roomId, "join", "leave", lastEventId); - const eventsPromise = responsePromise.then((response) => response.chunk); + const eventsPromise = responsePromise.then((response) => { + return response.chunk.map(this.getEventMapper()) + }); await room.loadOutOfBandMembers(eventsPromise); }; diff --git a/src/models/room.js b/src/models/room.js index f09fc659d..cbb35f3a7 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -232,21 +232,20 @@ Room.prototype.needsOutOfBandMembers = function() { /** * Loads the out-of-band members from the promise passed in - * @param {Promise} eventsPromise promise with array with state events + * @param {Promise<[MatrixEvent]>} eventsPromise state events for members */ Room.prototype.loadOutOfBandMembers = async function(eventsPromise) { - if (!this.membersNeedLoading()) { + if (!this.needsOutOfBandMembers()) { return; } this.currentState.markOutOfBandMembersStarted(); - let eventPojos = null; + let events = null; try { - eventPojos = await eventsPromise; + events = await eventsPromise; } catch (err) { this.currentState.markOutOfBandMembersFailed(); throw err; //rethrow so calling code is aware operation failed } - const events = eventPojos.map(this.client.getEventMapper()); this.currentState.setOutOfBandMembers(events); }; From 420a88c776d35b73d9062be4361d5c52c998ffba Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Jul 2018 13:02:52 +0200 Subject: [PATCH 067/472] remove obsolete flag --- src/models/room.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index cbb35f3a7..e2870bacf 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -171,8 +171,6 @@ function Room(roomId, opts) { // read by megolm; boolean value - null indicates "use global value" this._blacklistUnverifiedDevices = null; - // in case of lazy loading, to keep track of loading state - this._membersNeedLoading = true; } utils.inherits(Room, EventEmitter); From f6fafeaafb12764c989cc9082a127331e8a0805a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Jul 2018 14:12:07 +0200 Subject: [PATCH 068/472] 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. --- src/models/room.js | 20 ++++++++++++++++++++ src/sync.js | 4 ++++ 2 files changed, 24 insertions(+) diff --git a/src/models/room.js b/src/models/room.js index e2870bacf..bbd778859 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -171,6 +171,7 @@ function Room(roomId, opts) { // read by megolm; boolean value - null indicates "use global value" this._blacklistUnverifiedDevices = null; + this._syncedMembership = null; } utils.inherits(Room, EventEmitter); @@ -214,11 +215,30 @@ Room.prototype.getLiveTimeline = function() { return this.getUnfilteredTimelineSet().getLiveTimeline(); }; +/** + * @return {string} the id of the last event in the live timeline + */ Room.prototype.getLastEventId = function() { const liveEvents = this.getLiveTimeline().getEvents(); return liveEvents.length ? liveEvents[liveEvents.length - 1].getId() : undefined; }; +/** + * @return {string} the membership type (join | leave | invite) this room was received as during sync + */ +Room.prototype.getSyncedMembership = function() { + return this._syncedMembership; +}; + + +/** + * Sets the membership this room was received as during sync + * @param {string} membership join | leave | invite + */ +Room.prototype.setSyncedMembership = function(membership) { + this._syncedMembership = membership; +}; + /** * 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. diff --git a/src/sync.js b/src/sync.js index 9e15aebe8..2cd190bd4 100644 --- a/src/sync.js +++ b/src/sync.js @@ -959,6 +959,7 @@ SyncApi.prototype._processSyncResponse = async function( // Handle invites inviteRooms.forEach(function(inviteObj) { const room = inviteObj.room; + room.setSyncedMembership("invite"); const stateEvents = self._mapSyncEventsFormat(inviteObj.invite_state, room); self._processRoomEvents(room, stateEvents); @@ -975,6 +976,7 @@ SyncApi.prototype._processSyncResponse = async function( // Handle joins await Promise.mapSeries(joinRooms, async function(joinObj) { const room = joinObj.room; + room.setSyncedMembership("join"); const stateEvents = self._mapSyncEventsFormat(joinObj.state, room); const timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room); const ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral); @@ -1090,6 +1092,8 @@ SyncApi.prototype._processSyncResponse = async function( // Handle leaves (e.g. kicked rooms) leaveRooms.forEach(function(leaveObj) { const room = leaveObj.room; + room.setSyncedMembership("leave"); + const stateEvents = self._mapSyncEventsFormat(leaveObj.state, room); const timelineEvents = From 83c6615d6ed652b70b90692ad40a43e29cc34d01 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Jul 2018 14:53:41 +0200 Subject: [PATCH 069/472] move me || syncedmembership code into room.getmymembership --- src/models/room.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index bbd778859..8c2a345aa 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -224,13 +224,19 @@ Room.prototype.getLastEventId = function() { }; /** - * @return {string} the membership type (join | leave | invite) this room was received as during sync + * @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.getSyncedMembership = function() { +Room.prototype.getMyMembership = function(myUserId) { + if (myUserId) { + const me = this.getMember(myUserId); + if (me) { + return me.membership; + } + } return this._syncedMembership; }; - /** * Sets the membership this room was received as during sync * @param {string} membership join | leave | invite From 097e7df7c96f57dcf70a67c27497685ba5b518c7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Jul 2018 16:01:20 +0200 Subject: [PATCH 070/472] fix lint --- spec/unit/room.spec.js | 2 +- src/client.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 85d77269e..dfbf17ec4 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1293,7 +1293,7 @@ describe("Room", function() { }); const room = new Room(roomId); - const promise2 = Promise.resolve([memberEvent2]) + const promise2 = Promise.resolve([memberEvent2]); const promise1 = promise2.then(() => [memberEvent]); await room.loadOutOfBandMembers(promise1); diff --git a/src/client.js b/src/client.js index 0560ff3d1..b75fcf917 100644 --- a/src/client.js +++ b/src/client.js @@ -771,7 +771,7 @@ MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { const lastEventId = room.getLastEventId(); const responsePromise = this.members(roomId, "join", "leave", lastEventId); const eventsPromise = responsePromise.then((response) => { - return response.chunk.map(this.getEventMapper()) + return response.chunk.map(this.getEventMapper()); }); await room.loadOutOfBandMembers(eventsPromise); }; From 21ba4f71f6cfc908807f8f634adb0efbc8cea3d1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Jul 2018 16:20:48 +0200 Subject: [PATCH 071/472] jsdoc doesn't like generic promise type annotations --- src/models/room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room.js b/src/models/room.js index 8c2a345aa..51da8d391 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -256,7 +256,7 @@ Room.prototype.needsOutOfBandMembers = function() { /** * Loads the out-of-band members from the promise passed in - * @param {Promise<[MatrixEvent]>} eventsPromise state events for members + * @param {Promise} eventsPromise promise that resolves to an array with membership MatrixEvents for the members */ Room.prototype.loadOutOfBandMembers = async function(eventsPromise) { if (!this.needsOutOfBandMembers()) { From 8fa87f8ba5a213b89602f6cad31e8728a5115e1a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Jul 2018 18:38:14 +0200 Subject: [PATCH 072/472] make room summary available to Room from either the sync accumulator or the /sync endpoint --- src/models/room-state.js | 4 ++++ src/models/room.js | 17 +++++++++++++++++ src/sync-accumulator.js | 6 ++++++ src/sync.js | 8 ++++++++ 4 files changed, 35 insertions(+) diff --git a/src/models/room-state.js b/src/models/room-state.js index 4fe608bbf..2ed858ab8 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -102,6 +102,10 @@ RoomState.prototype.getJoinedMemberCount = function() { return this._joinedMemberCount; }; +RoomState.prototype.setJoinedMemberCount = function(count) { + this._joinedMemberCount = count; +} + /** * Get all RoomMembers in this room. * @return {Array} A list of RoomMembers. diff --git a/src/models/room.js b/src/models/room.js index 51da8d391..7f546c946 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -172,6 +172,7 @@ function Room(roomId, opts) { // read by megolm; boolean value - null indicates "use global value" this._blacklistUnverifiedDevices = null; this._syncedMembership = null; + this._summaryHeroes = null; } utils.inherits(Room, EventEmitter); @@ -377,6 +378,22 @@ Room.prototype.setUnreadNotificationCount = function(type, count) { this._notificationCounts[type] = count; }; +Room.prototype.setSummary = function(summary) { + const heros = summary["m.heros"]; + const count = summary["m.joined_member_count"]; + if (Number.isInteger(count)) { + this.currentState.setJoinedMemberCount(count); + } + this._summaryHeroes = heros; + + const oldName = this.name; + this.name = calculateRoomName(this, userId); + + if (oldName !== this.name) { + this.emit("Room.name", this); + } +} + /** * Whether to send encrypted messages to devices within this room. * @param {Boolean} value true to blacklist unverified devices, null diff --git a/src/sync-accumulator.js b/src/sync-accumulator.js index 7369c9b8a..add58adb1 100644 --- a/src/sync-accumulator.js +++ b/src/sync-accumulator.js @@ -63,6 +63,7 @@ class SyncAccumulator { // { event: $event, token: null|token }, // ... // ], + // _summary: { m.heros: [ $user_id ], m.joined_member_count: $count } // _accountData: { $event_type: json }, // _unreadNotifications: { ... unread_notifications JSON ... }, // _readReceipts: { $user_id: { data: $json, eventId: $event_id }} @@ -242,6 +243,7 @@ class SyncAccumulator { _timeline: [], _accountData: Object.create(null), _unreadNotifications: {}, + _summary: {}, _readReceipts: {}, }; } @@ -258,6 +260,9 @@ class SyncAccumulator { if (data.unread_notifications) { currentData._unreadNotifications = data.unread_notifications; } + if (data.summary) { + currentData._summary = data.summary; + } if (data.ephemeral && data.ephemeral.events) { data.ephemeral.events.forEach((e) => { @@ -428,6 +433,7 @@ class SyncAccumulator { prev_batch: null, }, unread_notifications: roomData._unreadNotifications, + summary: roomData._summary, }; // Add account data Object.keys(roomData._accountData).forEach((evType) => { diff --git a/src/sync.js b/src/sync.js index 2cd190bd4..8e348b6f3 100644 --- a/src/sync.js +++ b/src/sync.js @@ -823,6 +823,7 @@ SyncApi.prototype._processSyncResponse = async function( // state: { events: [] }, // timeline: { events: [], prev_batch: $token, limited: true }, // ephemeral: { events: [] }, + // summary: { m.heros: [ $userId ], m.joined_member_count: $count } // account_data: { events: [] }, // unread_notifications: { // highlight_count: 0, @@ -1056,6 +1057,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. From 5811ebd6f336f156e83c834fb6401457360a875d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Jul 2018 19:41:36 +0200 Subject: [PATCH 073/472] Support summary heroes in room name calculation Also clean-up algorithm, and remove assumption that we have all members as much as possible --- src/models/room.js | 140 ++++++++++++++++++++++----------------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 7f546c946..12aab5201 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1295,87 +1295,87 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { return alias; } + const joinedMemberCount = room.currentState.getJoinedMemberCount(); + const invitedMemberCount = room.currentState.getMembers().reduce((count, m) => { + return m.membership === 'invite' ? count + 1 : count; + }, 0); + const inviteJoinCount = joinedMemberCount + invitedMemberCount; + // 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 - ); + let otherMembers = null; + if (room._summaryHeroes) { + // if we have a summary, the member state events should have been in the room state + otherMembers = room._summaryHeroes.map((userId) => room.currentState.getMember(userId)); + } else { + otherMembers = room.currentState.getMembers().filter((m) => { + return m.userId !== userId && (m.membership === "invite" || m.membership === "join"); + }); + // sort by userId, as specified in the spec + otherMembers.sort((a, b) => a.userId.localeCompare(b.userId)); + otherMembers = otherMembers.slice(0, 5); //only 5 first members, immitate summary api + } + + const me = room.currentState.getMember(userId); // 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; + // XXX: diverges from spec + if (me && me.membership == "invite" && me.events.member) { + const inviterId = me.events.member.getSender(); + if (inviterId) { + // try extract who invited us to the room + const inviterMember = room.currentState.getMember(inviterId); + return inviterMember ? inviterMember : inviterId; } else { return "Room Invite"; } } + if (otherMembers.length) { + return memberListToRoomName(otherMembers, 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 allMembers = room.currentState.getMembers().filter((m) => { + return m.userId !== userId; + }); + const leftMembers = allMembers.filter((m) => { + return m.membership === "leave"; + }); + + if (leftMembers.length) { + return `Empty room (was ${memberListToRoomName(leftMembers)})`; + } + + if (me) { + const thirdPartyInvites = + room.currentState.getStateEvents("m.room.third_party_invite"); + + if (thirdPartyInvites && thirdPartyInvites.length) { + const thirdPartyNames = thirdPartyInvites.map(i => { + return {name: i.getContent().display_name}; + }); + + return `Inviting ${memberListToRoomName(thirdPartyNames)}`; } - } else if (otherMembers.length === 1) { - return otherMembers[0].name; - } else if (otherMembers.length === 2) { - return ( - otherMembers[0].name + " and " + otherMembers[1].name - ); + } + + const notLeftMembers = allMembers.filter((m) => { + return m.membership !== "leave"; + }); + + if (notLeftMembers.length) { + return `Empty room (was ${memberListToRoomName(leftMembers, leftMembers.length)})`; } else { - return ( - otherMembers[0].name + " and " + (otherMembers.length - 1) + " others" - ); + // there really isn't anyone in this room... + return "Empty room"; + } +} + +function memberListToRoomName(members, count = members.length) { + switch (members.length) { + case 0: return null; + case 1: return members[0].name; + case 2: return members[0].name + " and " + members[1].name; + default: return members[0].name + " and " + (count - 1) + " others"; } } From 0cb533beca2fd8d911c9afa8083c1a953a656f8f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 23 Jul 2018 19:48:44 +0200 Subject: [PATCH 074/472] no need to recalculate name here, as recalculate already does this --- src/models/room.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 12aab5201..b58ed1e8f 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -385,13 +385,6 @@ Room.prototype.setSummary = function(summary) { this.currentState.setJoinedMemberCount(count); } this._summaryHeroes = heros; - - const oldName = this.name; - this.name = calculateRoomName(this, userId); - - if (oldName !== this.name) { - this.emit("Room.name", this); - } } /** From 148876f5976ad3fbb4078ef9b46016c5c0f270d2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 24 Jul 2018 09:18:56 +0200 Subject: [PATCH 075/472] sorting should happen always --- src/models/room.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index b58ed1e8f..94abbf689 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1303,10 +1303,10 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { otherMembers = room.currentState.getMembers().filter((m) => { return m.userId !== userId && (m.membership === "invite" || m.membership === "join"); }); - // sort by userId, as specified in the spec - otherMembers.sort((a, b) => a.userId.localeCompare(b.userId)); - otherMembers = otherMembers.slice(0, 5); //only 5 first members, immitate summary api } + otherMembers.sort((a, b) => a.userId.localeCompare(b.userId)); + // sort by userId, as specified in the spec + otherMembers = otherMembers.slice(0, 5); //only 5 first members, immitate _summaryHeroes const me = room.currentState.getMember(userId); From fbdce27db2dafb2e29912f2e5ac6b189cec7ba4e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Jul 2018 17:52:52 +0200 Subject: [PATCH 076/472] m.heros => m.heroes --- src/models/room.js | 2 +- src/sync-accumulator.js | 2 +- src/sync.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 94abbf689..fd4627768 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -379,7 +379,7 @@ Room.prototype.setUnreadNotificationCount = function(type, count) { }; Room.prototype.setSummary = function(summary) { - const heros = summary["m.heros"]; + const heros = summary["m.heroes"]; const count = summary["m.joined_member_count"]; if (Number.isInteger(count)) { this.currentState.setJoinedMemberCount(count); diff --git a/src/sync-accumulator.js b/src/sync-accumulator.js index add58adb1..3ff71af77 100644 --- a/src/sync-accumulator.js +++ b/src/sync-accumulator.js @@ -63,7 +63,7 @@ class SyncAccumulator { // { event: $event, token: null|token }, // ... // ], - // _summary: { m.heros: [ $user_id ], m.joined_member_count: $count } + // _summary: { m.heroes: [ $user_id ], m.joined_member_count: $count } // _accountData: { $event_type: json }, // _unreadNotifications: { ... unread_notifications JSON ... }, // _readReceipts: { $user_id: { data: $json, eventId: $event_id }} diff --git a/src/sync.js b/src/sync.js index 8e348b6f3..c3c8970e8 100644 --- a/src/sync.js +++ b/src/sync.js @@ -823,7 +823,7 @@ SyncApi.prototype._processSyncResponse = async function( // state: { events: [] }, // timeline: { events: [], prev_batch: $token, limited: true }, // ephemeral: { events: [] }, - // summary: { m.heros: [ $userId ], m.joined_member_count: $count } + // summary: { m.heroes: [ $userId ], m.joined_member_count: $count } // account_data: { events: [] }, // unread_notifications: { // highlight_count: 0, From a5b3869e9f232008f2af7cf1161a4d2300ef7d29 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Jul 2018 18:02:36 +0200 Subject: [PATCH 077/472] 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 --- src/models/room-state.js | 29 +++++++++++++++++++++++++++++ src/models/room.js | 16 +++++++++++----- src/sync-accumulator.js | 16 ++++++++++++++-- src/sync.js | 6 +++++- 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/models/room-state.js b/src/models/room-state.js index 2ed858ab8..69fe27abc 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -79,6 +79,8 @@ function RoomState(roomId, oobMemberFlags = undefined) { this._userIdsToDisplayNames = {}; this._tokenToInvite = {}; // 3pid invite state_key to m.room.member invite this._joinedMemberCount = null; // cache of the number of joined members + this._invitedMemberCount = null; + if (!oobMemberFlags) { oobMemberFlags = { status: OOB_STATUS_NOTSTARTED, @@ -102,9 +104,33 @@ RoomState.prototype.getJoinedMemberCount = function() { 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._joinedMemberCount = 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._invitedMemberCount === null) { + this._invitedMemberCount = this.getMembers().filter((m) => { + return m.membership === 'invite'; + }).length; + } + 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._invitedMemberCount = count; +} /** * Get all RoomMembers in this room. @@ -193,6 +219,9 @@ RoomState.prototype.clone = function() { // Ugly hack: see above this._oobMemberFlags.status = status; + copy.setJoinedMemberCount(this.getJoinedMemberCount()); + copy.setInvitedMemberCount(this.getInvitedMemberCount()); + // copy out of band flags if needed if (this._oobMemberFlags.status == OOB_STATUS_FINISHED) { // copy markOutOfBand flags diff --git a/src/models/room.js b/src/models/room.js index fd4627768..ff5dcb1f3 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -379,12 +379,18 @@ Room.prototype.setUnreadNotificationCount = function(type, count) { }; Room.prototype.setSummary = function(summary) { - const heros = summary["m.heroes"]; - const count = summary["m.joined_member_count"]; - if (Number.isInteger(count)) { - this.currentState.setJoinedMemberCount(count); + 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 (heroes) { + this._summaryHeroes = heroes; } - this._summaryHeroes = heros; } /** diff --git a/src/sync-accumulator.js b/src/sync-accumulator.js index 3ff71af77..1a617d0ab 100644 --- a/src/sync-accumulator.js +++ b/src/sync-accumulator.js @@ -63,7 +63,11 @@ class SyncAccumulator { // { event: $event, token: null|token }, // ... // ], - // _summary: { m.heroes: [ $user_id ], m.joined_member_count: $count } + // _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 }} @@ -261,7 +265,15 @@ class SyncAccumulator { currentData._unreadNotifications = data.unread_notifications; } if (data.summary) { - currentData._summary = 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) { diff --git a/src/sync.js b/src/sync.js index c3c8970e8..c912d62d0 100644 --- a/src/sync.js +++ b/src/sync.js @@ -823,7 +823,11 @@ SyncApi.prototype._processSyncResponse = async function( // state: { events: [] }, // timeline: { events: [], prev_batch: $token, limited: true }, // ephemeral: { events: [] }, - // summary: { m.heroes: [ $userId ], m.joined_member_count: $count } + // summary: { + // m.heroes: [ $user_id ], + // m.joined_member_count: $count, + // m.invited_member_count: $count + // }, // account_data: { events: [] }, // unread_notifications: { // highlight_count: 0, From e61c6b89c84a83bd92d1c1018d2ddb7b4bad9f05 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Jul 2018 18:36:41 +0200 Subject: [PATCH 078/472] bring room name calculation in line with summary spec, while maintaining some backwards compatibility --- src/models/room.js | 69 +++++++++++++++++----------------------------- 1 file changed, 25 insertions(+), 44 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index ff5dcb1f3..b3b394b76 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1295,9 +1295,7 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { } const joinedMemberCount = room.currentState.getJoinedMemberCount(); - const invitedMemberCount = room.currentState.getMembers().reduce((count, m) => { - return m.membership === 'invite' ? count + 1 : count; - }, 0); + const invitedMemberCount = room.currentState.getInvitedMemberCount(); const inviteJoinCount = joinedMemberCount + invitedMemberCount; // get members that are NOT ourselves and are actually in the room. @@ -1309,62 +1307,45 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { otherMembers = room.currentState.getMembers().filter((m) => { return m.userId !== userId && (m.membership === "invite" || m.membership === "join"); }); - } - otherMembers.sort((a, b) => a.userId.localeCompare(b.userId)); - // sort by userId, as specified in the spec - otherMembers = otherMembers.slice(0, 5); //only 5 first members, immitate _summaryHeroes - - const me = room.currentState.getMember(userId); - - // TODO: Localisation - // XXX: diverges from spec - if (me && me.membership == "invite" && me.events.member) { - const inviterId = me.events.member.getSender(); - if (inviterId) { - // try extract who invited us to the room - const inviterMember = room.currentState.getMember(inviterId); - return inviterMember ? inviterMember : inviterId; - } else { - return "Room Invite"; - } + // 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); } - if (otherMembers.length) { + if (inviteJoinCount) { return memberListToRoomName(otherMembers, inviteJoinCount); } - const allMembers = room.currentState.getMembers().filter((m) => { - return m.userId !== userId; - }); - const leftMembers = allMembers.filter((m) => { - return m.membership === "leave"; - }); - - if (leftMembers.length) { - return `Empty room (was ${memberListToRoomName(leftMembers)})`; - } - - if (me) { + const myMembership = room.getMyMembership(userId); + // 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 => { + const thirdPartyNames = thirdPartyInvites.map((i) => { return {name: i.getContent().display_name}; }); return `Inviting ${memberListToRoomName(thirdPartyNames)}`; } } - - const notLeftMembers = allMembers.filter((m) => { - return m.membership !== "leave"; - }); - - if (notLeftMembers.length) { - return `Empty room (was ${memberListToRoomName(leftMembers, leftMembers.length)})`; - } else { - // there really isn't anyone in this room... + // let's try to figure out who was here before + let leftMembers = otherMembers; + // if we didn't have heroes, try finding them in the room state + if(!leftMembers.length) { + leftMembers = room.currentState.getMembers().filter((m) => { + return m.userId !== userId && + m.membership !== "invite" && + m.membership !== "join"; + }); + } + if(leftMembers.length) { + return `Empty room (was ${memberListToRoomName(leftMembers)})`; + } + else { return "Empty room"; } } From 9541aa7dbf948864f60799f9388f262a6641bb9a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 25 Jul 2018 18:45:52 +0200 Subject: [PATCH 079/472] fix lint --- src/models/room-state.js | 4 ++-- src/models/room.js | 23 +++++++++++++---------- src/sync-accumulator.js | 2 +- src/sync.js | 2 +- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/models/room-state.js b/src/models/room-state.js index 69fe27abc..3c008d4ba 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -110,7 +110,7 @@ RoomState.prototype.getJoinedMemberCount = function() { */ RoomState.prototype.setJoinedMemberCount = function(count) { this._joinedMemberCount = count; -} +}; /** * Returns the number of invited members in this room * @return {integer} The number of members in this room whose membership is 'invite' @@ -130,7 +130,7 @@ RoomState.prototype.getInvitedMemberCount = function() { */ RoomState.prototype.setInvitedMemberCount = function(count) { this._invitedMemberCount = count; -} +}; /** * Get all RoomMembers in this room. diff --git a/src/models/room.js b/src/models/room.js index b3b394b76..0defae4bf 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -391,7 +391,7 @@ Room.prototype.setSummary = function(summary) { if (heroes) { this._summaryHeroes = heroes; } -} +}; /** * Whether to send encrypted messages to devices within this room. @@ -1301,11 +1301,15 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { // get members that are NOT ourselves and are actually in the room. let otherMembers = null; if (room._summaryHeroes) { - // if we have a summary, the member state events should have been in the room state - otherMembers = room._summaryHeroes.map((userId) => room.currentState.getMember(userId)); + // if we have a summary, the member state events + // should be in the room state + otherMembers = room._summaryHeroes.map((userId) => { + return room.currentState.getMember(userId); + }); } else { otherMembers = room.currentState.getMembers().filter((m) => { - return m.userId !== userId && (m.membership === "invite" || m.membership === "join"); + 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)); @@ -1323,7 +1327,7 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { if (myMembership == 'join') { const thirdPartyInvites = room.currentState.getStateEvents("m.room.third_party_invite"); - + if (thirdPartyInvites && thirdPartyInvites.length) { const thirdPartyNames = thirdPartyInvites.map((i) => { return {name: i.getContent().display_name}; @@ -1344,17 +1348,16 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { } if(leftMembers.length) { return `Empty room (was ${memberListToRoomName(leftMembers)})`; - } - else { + } else { return "Empty room"; } } function memberListToRoomName(members, count = members.length) { switch (members.length) { - case 0: return null; - case 1: return members[0].name; - case 2: return members[0].name + " and " + members[1].name; + case 0: return null; + case 1: return members[0].name; + case 2: return members[0].name + " and " + members[1].name; default: return members[0].name + " and " + (count - 1) + " others"; } } diff --git a/src/sync-accumulator.js b/src/sync-accumulator.js index 1a617d0ab..634275cb9 100644 --- a/src/sync-accumulator.js +++ b/src/sync-accumulator.js @@ -268,7 +268,7 @@ class SyncAccumulator { 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]; diff --git a/src/sync.js b/src/sync.js index c912d62d0..d2f6dc760 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1067,7 +1067,7 @@ SyncApi.prototype._processSyncResponse = async function( 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. From 00bf5bdf694180d022101c2ec9861625d208e485 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 26 Jul 2018 16:26:49 +0200 Subject: [PATCH 080/472] 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. --- spec/unit/room.spec.js | 50 ++++++++++++++++++++++++------------------ src/models/room.js | 6 +---- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index dfbf17ec4..1c95af135 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -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() { @@ -570,40 +571,47 @@ 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); }); }); diff --git a/src/models/room.js b/src/models/room.js index 0defae4bf..f12fbfa75 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -517,11 +517,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline, * @return {RoomMember} The member or null. */ Room.prototype.getMember = function(userId) { - const member = this.currentState.members[userId]; - if (!member) { - return null; - } - return member; + return this.currentState.getMember(userId); }; /** From f5ff5dc3e0d30a6eade7663d973ca7473d8eca5f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 26 Jul 2018 17:48:40 +0200 Subject: [PATCH 081/472] 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 --- spec/unit/room.spec.js | 97 ++++++++++-------------------------------- src/models/room.js | 3 +- 2 files changed, 25 insertions(+), 75 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 1c95af135..641d75305 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -617,33 +617,29 @@ describe("Room", function() { }); 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) { @@ -655,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); }); describe("Room.recalculate => Stripped State Events", function() { @@ -712,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: "", @@ -724,29 +678,26 @@ 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, - }); + 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(); + expect(room.name).toEqual(roomName); }); }); @@ -867,8 +818,7 @@ describe("Room", function() { setJoinRule("public"); setRoomName(roomName); room.recalculate(userA); - const name = room.name; - expect(name).toEqual(roomName); + expect(room.name).toEqual(roomName); }); it("should return 'Empty room' for private (invite join_rules) rooms if" + @@ -876,8 +826,7 @@ describe("Room", function() { setJoinRule("invite"); addMember(userA); room.recalculate(userA); - const name = room.name; - expect(name).toEqual("Empty room"); + expect(room.name).toEqual("Empty room"); }); it("should return 'Empty room' for public (public join_rules) rooms if a" + diff --git a/src/models/room.js b/src/models/room.js index f12fbfa75..9f6fb9a82 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1351,7 +1351,8 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { function memberListToRoomName(members, count = members.length) { switch (members.length) { - case 0: return null; + // count would be 1 for a self-chat + case 0: return count <= 1 ? "Empty room" : null; case 1: return members[0].name; case 2: return members[0].name + " and " + members[1].name; default: return members[0].name + " and " + (count - 1) + " others"; From 3a389793fff4a297b29c540f5115477ba78e3c15 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 26 Jul 2018 17:54:23 +0200 Subject: [PATCH 082/472] fix sync accumulator test to include summary --- spec/unit/sync-accumulator.spec.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/unit/sync-accumulator.spec.js b/spec/unit/sync-accumulator.spec.js index 1a857fe65..1e7345b38 100644 --- a/spec/unit/sync-accumulator.spec.js +++ b/spec/unit/sync-accumulator.spec.js @@ -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", From d67bdbf088683b1f7a0967ceb140dade76b4f7b4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 26 Jul 2018 18:09:20 +0200 Subject: [PATCH 083/472] test accumulating summary fields --- spec/unit/sync-accumulator.spec.js | 54 ++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/spec/unit/sync-accumulator.spec.js b/spec/unit/sync-accumulator.spec.js index 1e7345b38..c4ffe439b 100644 --- a/spec/unit/sync-accumulator.spec.js +++ b/spec/unit/sync-accumulator.spec.js @@ -323,6 +323,60 @@ 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, + })); + let 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, + })); + + let 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) { From 20b4285849663270378402e25a7b7313834fe755 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 26 Jul 2018 18:44:08 +0200 Subject: [PATCH 084/472] add some tests for room name based on room summary + fix because it was actually broken --- spec/unit/room.spec.js | 48 ++++++++++++++++++++++++++++++++++++++++++ src/models/room.js | 25 +++++++++++++++------- 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 641d75305..a0893ff58 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -701,6 +701,54 @@ describe("Room", function() { }); }); + describe("Room.recalculate => Room Name using room summary", function() { + + it("should use room heroes if available", function() { + addMember(userB); + addMember(userC); + addMember(userD); + room.setSummary({ + "m.heroes": [userB, userC, userD] + }); + + room.recalculate(userA); + 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": 1 + }); + + room.recalculate(userA); + expect(room.name).toEqual(userB); + }); + + it("uses hero name from state", function() { + const name = "Mr B"; + addMember(userB, "join", {name}); + room.setSummary({ + "m.heroes": [userB], + }); + + room.recalculate(userA); + 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(userA); + expect(room.name).toEqual(`${name} and 99 others`); + }); + }); + describe("Room.recalculate => Room Name", function() { it("should return the names of members in a private (invite join_rules)" + " room if a room name and alias don't exist and there are >3 members.", diff --git a/src/models/room.js b/src/models/room.js index 9f6fb9a82..2fe073f70 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1300,7 +1300,8 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { // if we have a summary, the member state events // should be in the room state otherMembers = room._summaryHeroes.map((userId) => { - return room.currentState.getMember(userId); + const member = room.getMember(userId); + return member ? member : {name: userId}; }); } else { otherMembers = room.currentState.getMembers().filter((m) => { @@ -1349,13 +1350,21 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { } } -function memberListToRoomName(members, count = members.length) { - switch (members.length) { - // count would be 1 for a self-chat - case 0: return count <= 1 ? "Empty room" : null; - case 1: return members[0].name; - case 2: return members[0].name + " and " + members[1].name; - default: return members[0].name + " and " + (count - 1) + " others"; +function memberListToRoomName(members, count = (members.length + 1)) { + const countWithoutMe = count - 1; + if (!members.length) { + return count <= 1 ? "Empty room" : null; + } else if (members.length === 1 && countWithoutMe <= 1) { + return members[0].name; + } else if (members.length === 2 && countWithoutMe <= 2) { + return `${members[0].name} and ${members[1].name}`; + } else { + const plural = countWithoutMe > 1; + if (plural) { + return `${members[0].name} and ${countWithoutMe} others`; + } else { + return `${members[0].name} and 1 other`; + } } } From 576f7142c11f548bb7b13dc8211c6a472ba2e045 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 26 Jul 2018 18:52:14 +0200 Subject: [PATCH 085/472] just need the member names actually --- src/models/room.js | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 2fe073f70..aef780338 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1295,16 +1295,16 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { const inviteJoinCount = joinedMemberCount + invitedMemberCount; // get members that are NOT ourselves and are actually in the room. - let otherMembers = null; + let otherNames = null; if (room._summaryHeroes) { // if we have a summary, the member state events // should be in the room state - otherMembers = room._summaryHeroes.map((userId) => { + otherNames = room._summaryHeroes.map((userId) => { const member = room.getMember(userId); - return member ? member : {name: userId}; + return member ? member.name : userId; }); } else { - otherMembers = room.currentState.getMembers().filter((m) => { + let otherMembers = room.currentState.getMembers().filter((m) => { return m.userId !== userId && (m.membership === "invite" || m.membership === "join"); }); @@ -1312,10 +1312,11 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { 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 memberListToRoomName(otherMembers, inviteJoinCount); + return memberNamesToRoomName(otherNames, inviteJoinCount); } const myMembership = room.getMyMembership(userId); @@ -1327,43 +1328,43 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { if (thirdPartyInvites && thirdPartyInvites.length) { const thirdPartyNames = thirdPartyInvites.map((i) => { - return {name: i.getContent().display_name}; + return i.getContent().display_name; }); - return `Inviting ${memberListToRoomName(thirdPartyNames)}`; + return `Inviting ${memberNamesToRoomName(thirdPartyNames)}`; } } // let's try to figure out who was here before - let leftMembers = otherMembers; + let leftNames = otherNames; // if we didn't have heroes, try finding them in the room state - if(!leftMembers.length) { - leftMembers = room.currentState.getMembers().filter((m) => { + if(!leftNames.length) { + leftNames = room.currentState.getMembers().filter((m) => { return m.userId !== userId && m.membership !== "invite" && m.membership !== "join"; - }); + }).map((m) => m.name); } - if(leftMembers.length) { - return `Empty room (was ${memberListToRoomName(leftMembers)})`; + if(leftNames.length) { + return `Empty room (was ${memberNamesToRoomName(leftNames)})`; } else { return "Empty room"; } } -function memberListToRoomName(members, count = (members.length + 1)) { +function memberNamesToRoomName(names, count = (names.length + 1)) { const countWithoutMe = count - 1; - if (!members.length) { + if (!names.length) { return count <= 1 ? "Empty room" : null; - } else if (members.length === 1 && countWithoutMe <= 1) { - return members[0].name; - } else if (members.length === 2 && countWithoutMe <= 2) { - return `${members[0].name} and ${members[1].name}`; + } 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 `${members[0].name} and ${countWithoutMe} others`; + return `${names[0]} and ${countWithoutMe} others`; } else { - return `${members[0].name} and 1 other`; + return `${names[0]} and 1 other`; } } } From 230a9311a0e1f6517340865aaaa49e780604ca53 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 26 Jul 2018 19:03:46 +0200 Subject: [PATCH 086/472] actually need to subtract one from join+invite count as that includes the syncing user --- spec/unit/room.spec.js | 39 +++++++++++++++++++++++++++++++++++++-- src/models/room.js | 3 ++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index a0893ff58..639fffd3d 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -704,6 +704,7 @@ describe("Room", function() { 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); @@ -718,7 +719,7 @@ describe("Room", function() { it("missing hero member state reverts to mxid", function() { room.setSummary({ "m.heroes": [userB], - "m.joined_member_count": 1 + "m.joined_member_count": 2 }); room.recalculate(userA); @@ -727,6 +728,7 @@ describe("Room", function() { it("uses hero name from state", function() { const name = "Mr B"; + addMember(userA, "invite"); addMember(userB, "join", {name}); room.setSummary({ "m.heroes": [userB], @@ -745,8 +747,41 @@ describe("Room", function() { "m.invited_member_count": 50, }); room.recalculate(userA); - expect(room.name).toEqual(`${name} and 99 others`); + 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(userA); + 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(userA); + 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(userA); + expect(room.name).toEqual("Empty room"); + }); + }); describe("Room.recalculate => Room Name", function() { diff --git a/src/models/room.js b/src/models/room.js index aef780338..932b9a579 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1292,7 +1292,8 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { const joinedMemberCount = room.currentState.getJoinedMemberCount(); const invitedMemberCount = room.currentState.getInvitedMemberCount(); - const inviteJoinCount = joinedMemberCount + invitedMemberCount; + // -1 because these numbers include the syncing user + const inviteJoinCount = joinedMemberCount + invitedMemberCount - 1; // get members that are NOT ourselves and are actually in the room. let otherNames = null; From cf08901d02cefc1534fe466e8ea663a17e9a0b1d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 26 Jul 2018 19:08:00 +0200 Subject: [PATCH 087/472] fix lint --- spec/unit/room.spec.js | 12 +++++------- spec/unit/sync-accumulator.spec.js | 8 +++----- src/models/room.js | 2 +- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 639fffd3d..3a436342d 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -622,7 +622,7 @@ describe("Room", function() { type: "m.room.join_rules", room: roomId, user: userA, content: { join_rule: rule, }, event: true, - })]);; + })]); }; const setAliases = function(aliases, stateKey) { if (!stateKey) { @@ -702,14 +702,13 @@ describe("Room", function() { }); 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] + "m.heroes": [userB, userC, userD], }); room.recalculate(userA); @@ -719,7 +718,7 @@ describe("Room", function() { it("missing hero member state reverts to mxid", function() { room.setSummary({ "m.heroes": [userB], - "m.joined_member_count": 2 + "m.joined_member_count": 2, }); room.recalculate(userA); @@ -756,7 +755,7 @@ describe("Room", function() { addMember(userB, "join", {name: nameB}); addMember(userC, "join", {name: nameC}); room.setSummary({ - "m.heroes": [userB, userC] + "m.heroes": [userB, userC], }); room.recalculate(userA); expect(room.name).toEqual(`${nameB} and ${nameC}`); @@ -767,7 +766,7 @@ describe("Room", function() { addMember(userB, "join", {name: nameB}); addMember(userC, "join"); room.setSummary({ - "m.heroes": [userB] + "m.heroes": [userB], }); room.recalculate(userA); expect(room.name).toEqual(nameB); @@ -781,7 +780,6 @@ describe("Room", function() { room.recalculate(userA); expect(room.name).toEqual("Empty room"); }); - }); describe("Room.recalculate => Room Name", function() { diff --git a/spec/unit/sync-accumulator.spec.js b/spec/unit/sync-accumulator.spec.js index c4ffe439b..a29be7efd 100644 --- a/spec/unit/sync-accumulator.spec.js +++ b/spec/unit/sync-accumulator.spec.js @@ -355,7 +355,7 @@ describe("SyncAccumulator", function() { "m.heroes": ["@alice:bar"], "m.invited_member_count": 2, })); - let summary = sa.getJSON().roomsData.join["!foo:bar"].summary; + const summary = sa.getJSON().roomsData.join["!foo:bar"].summary; expect(summary["m.invited_member_count"]).toEqual(2); expect(summary["m.heroes"]).toEqual(["@alice:bar"]); }); @@ -369,14 +369,12 @@ describe("SyncAccumulator", function() { "m.heroes": ["@bob:bar"], "m.joined_member_count": 5, })); - - let summary = sa.getJSON().roomsData.join["!foo:bar"].summary; + 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) { diff --git a/src/models/room.js b/src/models/room.js index 932b9a579..0d0abc71f 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1355,7 +1355,7 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { function memberNamesToRoomName(names, count = (names.length + 1)) { const countWithoutMe = count - 1; if (!names.length) { - return count <= 1 ? "Empty room" : null; + return count <= 1 ? "Empty room" : null; } else if (names.length === 1 && countWithoutMe <= 1) { return names[0]; } else if (names.length === 2 && countWithoutMe <= 2) { From 1d71e7243ff34110d55522308afb370fc7155309 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 26 Jul 2018 19:12:21 +0200 Subject: [PATCH 088/472] no need to create new array here --- src/models/room-state.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/models/room-state.js b/src/models/room-state.js index 3c008d4ba..93252baf1 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -97,9 +97,9 @@ utils.inherits(RoomState, EventEmitter); */ RoomState.prototype.getJoinedMemberCount = function() { 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; }; @@ -117,9 +117,9 @@ RoomState.prototype.setJoinedMemberCount = function(count) { */ RoomState.prototype.getInvitedMemberCount = function() { if (this._invitedMemberCount === null) { - this._invitedMemberCount = this.getMembers().filter((m) => { - return m.membership === 'invite'; - }).length; + this._invitedMemberCount = this.getMembers().reduce((count, m) => { + return m.membership === 'invite' ? count + 1 : count; + }, 0); } return this._invitedMemberCount; }; From 6ce7170cf44c199ca196652751749cd920dd72c8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 26 Jul 2018 19:31:01 +0200 Subject: [PATCH 089/472] counts from summary api should override count members manually as members might not be complete --- spec/unit/room-state.spec.js | 38 +++++++++++++++++++++++++++++++++++- src/models/room-state.js | 26 ++++++++++++++++++++---- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/spec/unit/room-state.spec.js b/spec/unit/room-state.spec.js index 626232783..3c1b0e97d 100644 --- a/spec/unit/room-state.spec.js +++ b/spec/unit/room-state.spec.js @@ -11,6 +11,7 @@ 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; @@ -225,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, }); @@ -529,6 +529,42 @@ describe("RoomState", function() { }); }); + describe("setJoinedMemberCount", function() { + it("should, once used, override counting members from state", function() { + state.setStateEvents([ + utils.mkMembership({event: true, mship: "join", + user: userA, room: roomId}), + utils.mkMembership({event: true, mship: "invite", + user: userB, 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}), + utils.mkMembership({event: true, mship: "invite", + user: userB, 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("maySendEvent", function() { it("should say non-joined members may not send events", function() { diff --git a/src/models/room-state.js b/src/models/room-state.js index 93252baf1..65a6784c8 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -79,7 +79,15 @@ function RoomState(roomId, oobMemberFlags = undefined) { 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 = { @@ -96,6 +104,9 @@ 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().reduce((count, m) => { return m.membership === 'join' ? count + 1 : count; @@ -109,13 +120,16 @@ RoomState.prototype.getJoinedMemberCount = function() { * @param {number} count the amount of joined members */ RoomState.prototype.setJoinedMemberCount = function(count) { - this._joinedMemberCount = 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; @@ -129,7 +143,7 @@ RoomState.prototype.getInvitedMemberCount = function() { * @param {number} count the amount of invited members */ RoomState.prototype.setInvitedMemberCount = function(count) { - this._invitedMemberCount = count; + this._summaryInvitedMemberCount = count; }; /** @@ -219,8 +233,12 @@ RoomState.prototype.clone = function() { // Ugly hack: see above this._oobMemberFlags.status = status; - copy.setJoinedMemberCount(this.getJoinedMemberCount()); - copy.setInvitedMemberCount(this.getInvitedMemberCount()); + 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) { From d8f673ed5198bd68acabb2ee6b8da0250ca35d18 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 26 Jul 2018 19:43:05 +0200 Subject: [PATCH 090/472] make sure invited count cache gets reset when updating member --- spec/unit/room-state.spec.js | 82 ++++++++++++++++++++++++++++++++++-- src/models/room-state.js | 1 + 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/spec/unit/room-state.spec.js b/spec/unit/room-state.spec.js index 3c1b0e97d..1608fd4aa 100644 --- a/spec/unit/room-state.spec.js +++ b/spec/unit/room-state.spec.js @@ -529,13 +529,53 @@ describe("RoomState", function() { }); }); + 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}), - utils.mkMembership({event: true, mship: "invite", - user: userB, room: roomId}), ]); expect(state.getJoinedMemberCount()).toEqual(1); state.setJoinedMemberCount(100); @@ -552,8 +592,6 @@ describe("RoomState", function() { state.setStateEvents([ utils.mkMembership({event: true, mship: "join", user: userA, room: roomId}), - utils.mkMembership({event: true, mship: "invite", - user: userB, room: roomId}), ]); state.setJoinedMemberCount(100); const copy = state.clone(); @@ -565,6 +603,42 @@ describe("RoomState", function() { }); }); + 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 non-joined members may not send events", function() { diff --git a/src/models/room-state.js b/src/models/room-state.js index 65a6784c8..1cbf93a5d 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -376,6 +376,7 @@ RoomState.prototype._updateMember = function(member) { this.members[member.userId] = member; this._joinedMemberCount = null; + this._invitedMemberCount = null; }; /** From bb490faefe4de82bf9bb84be94c6970bf4e60a5e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 26 Jul 2018 19:44:07 +0200 Subject: [PATCH 091/472] fix lint --- spec/unit/room-state.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/unit/room-state.spec.js b/spec/unit/room-state.spec.js index 1608fd4aa..24f940184 100644 --- a/spec/unit/room-state.spec.js +++ b/spec/unit/room-state.spec.js @@ -533,7 +533,7 @@ describe("RoomState", function() { beforeEach(() => { state = new RoomState(roomId); }); - + it("should update after adding joined member", function() { state.setStateEvents([ utils.mkMembership({event: true, mship: "join", @@ -552,7 +552,7 @@ describe("RoomState", function() { beforeEach(() => { state = new RoomState(roomId); }); - + it("should update after adding invited member", function() { state.setStateEvents([ utils.mkMembership({event: true, mship: "invite", @@ -571,7 +571,7 @@ describe("RoomState", function() { beforeEach(() => { state = new RoomState(roomId); }); - + it("should, once used, override counting members from state", function() { state.setStateEvents([ utils.mkMembership({event: true, mship: "join", From ca3981fba8db7f700275b6a86a3c9d3cbb9d6db3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 30 Jul 2018 17:36:18 +0200 Subject: [PATCH 092/472] back-port infinite spinner fix as room state code changed considerably for lazy loading --- src/models/room-state.js | 45 ++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/models/room-state.js b/src/models/room-state.js index 1cbf93a5d..698b894b4 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -323,19 +323,8 @@ RoomState.prototype.setStateEvents = function(stateEvents) { event.getPrevContent().displayname; } - let member = self.members[userId]; - self._joinedMemberCount = null; - - if (!member) { - member = new RoomMember(event.getRoomId(), userId); - // add member to members before emitting any events, - // as event handlers often lookup the member - self.members[userId] = member; - self.emit("RoomState.newMember", event, self, member); - } - + const member = self._getOrCreateMember(userId, event); member.setMembershipEvent(event, self); - // if out of band members are loading, // mark the member as more recent if (self._oobMemberFlags.status == OOB_STATUS_INPROGRESS) { @@ -357,6 +346,28 @@ 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()] = {}; @@ -455,8 +466,7 @@ RoomState.prototype._setOutOfBandMember = function(stateEvent) { } } - const member = - existingMember ? existingMember : new RoomMember(this.roomId, userId); + const member = this._getOrCreateMember(userId, stateEvent); member.setMembershipEvent(stateEvent); // needed to know which members need to be stored seperately // as the are not part of the sync accumulator @@ -467,12 +477,7 @@ RoomState.prototype._setOutOfBandMember = function(stateEvent) { this._setStateEvent(stateEvent); this._updateMember(member); - - if (existingMember) { - this.emit("RoomState.members", {}, this, member); - } else { - this.emit('RoomState.newMember', {}, this, member); - } + this.emit("RoomState.members", {}, stateEvent, member); }; /** From d366ec9c4892cf4c60b10888163ec224a5606eab Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 18 Jul 2018 17:10:23 +0200 Subject: [PATCH 093/472] prototype how we could store ll members --- src/client.js | 23 ++++++---- src/store/indexeddb-local-backend.js | 60 ++++++++++++++++++++++++++- src/store/indexeddb-remote-backend.js | 7 ++++ src/store/indexeddb-store-worker.js | 6 +++ src/store/indexeddb.js | 8 ++++ 5 files changed, 96 insertions(+), 8 deletions(-) diff --git a/src/client.js b/src/client.js index b75fcf917..105606478 100644 --- a/src/client.js +++ b/src/client.js @@ -757,6 +757,20 @@ MatrixClient.prototype.getRoom = function(roomId) { return this.store.getRoom(roomId); }; +MatrixClient.prototype._loadMembers = async function(room) { + const roomId = room.roomId; + let rawMembersEvents = await this.store.getOutOfBandMembers(roomId); + if (rawMembersEvents.length == 0) { + const lastEventId = room.getLastEventId(); + const response = await this.members(roomId, "join", "leave", lastEventId); + rawMembersEvents = response.chunk; + // TODO don't block on writing + await this.store.setOutOfBandMembers(roomId, rawMembersEvents); + } + const memberEvents = rawMembersEvents.map(this.getEventMapper()); + return memberEvents; +}; + /** * Preloads the member list for the given room id, * in case lazy loading of memberships is in use. @@ -767,13 +781,8 @@ MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { if (!room || !room.needsOutOfBandMembers()) { return; } - - const lastEventId = room.getLastEventId(); - const responsePromise = this.members(roomId, "join", "leave", lastEventId); - const eventsPromise = responsePromise.then((response) => { - return response.chunk.map(this.getEventMapper()); - }); - await room.loadOutOfBandMembers(eventsPromise); + const membersPromise = this._loadMembers(room); + await room.loadOutOfBandMembers(membersPromise); }; /** diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index a928e1aa6..7d64c4d6d 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -19,7 +19,7 @@ import Promise from 'bluebird'; import SyncAccumulator from "../sync-accumulator"; import utils from "../utils"; -const VERSION = 1; +const VERSION = 2; function createDatabase(db) { // Make user store, clobber based on user ID. (userId property of User objects) @@ -33,6 +33,11 @@ function createDatabase(db) { db.createObjectStore("sync", { keyPath: ["clobber"] }); } +function upgradeSchemaV2(db) { + const llMembersStore = db.createObjectStore("lazy_loaded_members", { keyPath: ["roomId", "userId"]}); + llMembersStore.createIndex("room", "roomId"); +} + /** * Helper method to collect results from a Cursor and promiseify it. * @param {ObjectStore|Index} store The store to perform openCursor on. @@ -136,6 +141,9 @@ LocalIndexedDBStoreBackend.prototype = { if (oldVersion < 1) { // The database did not previously exist. createDatabase(db); } + if (oldVersion < 2) { + upgradeSchemaV2(db); + } // Expand as needed. }; @@ -187,6 +195,56 @@ LocalIndexedDBStoreBackend.prototype = { }); }, + getOutOfBandMembers: function(roomId) { + return new Promise((resolve, reject) =>{ + const tx = this.db.transaction(["lazy_loaded_members"], "readonly"); + const store = tx.objectStore("lazy_loaded_members"); + const roomIndex = store.index("room"); + const range = IDBKeyRange.only(roomId); + const request = roomIndex.openCursor(range); + + const members = []; + request.onsuccess = (event) => { + const cursor = event.target.result; + if (!cursor) { + return resolve(members); + } + const record = cursor.value; + members.push({ + userId: record.userId, + displayName: record.displayName, + membership: record.membership, + avatarUrl: record.avatarUrl, + }); + cursor.continue(); + }; + request.onerror = (err) => { + reject(err); + }; + }); + }, + + setOutOfBandMembers: function(roomId, members) { + function ignoreResult() {}; + // run everything in a promise so anything that throws will reject + return new Promise((resolve) =>{ + const tx = this.db.transaction(["lazy_loaded_members"], "readwrite"); + const store = tx.objectStore("lazy_loaded_members"); + const puts = members.map((m) => { + const record = { + roomId: roomId, + userId: m.userId, + displayName: m.displayName, + avatarUrl: m.avatarUrl, + membership: m.membership + }; + let putPromise = promiseifyRequest(store.put(record)); + return putPromise.then(ignoreResult); + }); + resolve(Promise.all(puts).then(ignoreResult)); + }); + }, + /** * Clear the entire database. This should be used when logging out of a client * to prevent mixing data between accounts. diff --git a/src/store/indexeddb-remote-backend.js b/src/store/indexeddb-remote-backend.js index 2221633e4..1594eef26 100644 --- a/src/store/indexeddb-remote-backend.js +++ b/src/store/indexeddb-remote-backend.js @@ -87,6 +87,13 @@ RemoteIndexedDBStoreBackend.prototype = { return this._doCmd('syncToDatabase', [users]); }, + getOutOfBandMembers: function(roomId) { + return this._doCmd('getOutOfBandMembers', [roomId]); + }, + + setOutOfBandMembers: function(roomId, members) { + return this._doCmd('setOutOfBandMembers', [roomId, members]); + }, /** * Load all user presence events from the database. This is not cached. diff --git a/src/store/indexeddb-store-worker.js b/src/store/indexeddb-store-worker.js index d32df7274..021156afc 100644 --- a/src/store/indexeddb-store-worker.js +++ b/src/store/indexeddb-store-worker.js @@ -92,6 +92,12 @@ class IndexedDBStoreWorker { case 'getNextBatchToken': prom = this.backend.getNextBatchToken(); break; + case 'getOutOfBandMembers': + prom = this.backend.getOutOfBandMembers(msg.args[0]); + break; + case 'setOutOfBandMembers': + prom = this.backend.setOutOfBandMembers(msg.args[0], msg.args[1]); + break; } if (prom === undefined) { diff --git a/src/store/indexeddb.js b/src/store/indexeddb.js index d47e35544..0086eb531 100644 --- a/src/store/indexeddb.js +++ b/src/store/indexeddb.js @@ -219,4 +219,12 @@ IndexedDBStore.prototype.setSyncData = function(syncData) { return this.backend.setSyncData(syncData); }; +IndexedDBStore.prototype.getOutOfBandMembers = function(roomId) { + return this.backend.getOutOfBandMembers(roomId); +}; + +IndexedDBStore.prototype.setOutOfBandMembers = function(roomId, members) { + return this.backend.setOutOfBandMembers(roomId, members); +}; + module.exports.IndexedDBStore = IndexedDBStore; From 2b9c8344760213b983cd852b4cfa8c6405ad0a41 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 19 Jul 2018 16:32:18 +0200 Subject: [PATCH 094/472] add comment to clarify how we avoid race --- src/client.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client.js b/src/client.js index 105606478..14e160eb9 100644 --- a/src/client.js +++ b/src/client.js @@ -781,6 +781,10 @@ MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { if (!room || !room.needsOutOfBandMembers()) { return; } + // Note that we don't await _loadMembers here first. + // setLazyLoadedMembers sets a flag before it awaits the promise passed in + // to avoid a race when calling membersNeedLoading/loadOutOfBandMembers + // in fast succession, before the first promise resolves. const membersPromise = this._loadMembers(room); await room.loadOutOfBandMembers(membersPromise); }; From c618ce46250672f4dbe2a341193f67e506bc2184 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 30 Jul 2018 14:52:02 +0200 Subject: [PATCH 095/472] store only out of band members --- src/client.js | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/client.js b/src/client.js index 14e160eb9..8105ea384 100644 --- a/src/client.js +++ b/src/client.js @@ -759,8 +759,11 @@ MatrixClient.prototype.getRoom = function(roomId) { MatrixClient.prototype._loadMembers = async function(room) { const roomId = room.roomId; + // were the members loaded from the server? + let fromServer = false; let rawMembersEvents = await this.store.getOutOfBandMembers(roomId); if (rawMembersEvents.length == 0) { + fromServer = true; const lastEventId = room.getLastEventId(); const response = await this.members(roomId, "join", "leave", lastEventId); rawMembersEvents = response.chunk; @@ -768,7 +771,7 @@ MatrixClient.prototype._loadMembers = async function(room) { await this.store.setOutOfBandMembers(roomId, rawMembersEvents); } const memberEvents = rawMembersEvents.map(this.getEventMapper()); - return memberEvents; + return {memberEvents, fromServer}; }; /** @@ -785,8 +788,30 @@ MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { // setLazyLoadedMembers sets a flag before it awaits the promise passed in // to avoid a race when calling membersNeedLoading/loadOutOfBandMembers // in fast succession, before the first promise resolves. - const membersPromise = this._loadMembers(room); + let membersPromise = this._loadMembers(room); + // intercept whether we need to store oob members afterwards + let membersNeedStoring = false; + membersPromise = membersPromise.then(({memberEvents, fromServer}) => { + membersNeedStoring = fromServer; + return memberEvents; + }); await room.loadOutOfBandMembers(membersPromise); + // if loadOutOfBandMembers throws, this wont be called + // but that's fine as we don't want to store members + // that caused an error. + if (membersNeedStoring) { + const rawMembersEvents = room.currentState.getMembers() + .filter((m) => m.isOutOfBand()) + .map((m) => m.events.member.event); + // TODO: probably need a way to mark a room as lazy loaded + // even though we didn't store any members, as we'll just + // lazy loaded the room in every session. This is a likely + // scenario for DM's where all the members would likely + // be known without lazy loading. + if (rawMembersEvents.length) { + await this.store.setOutOfBandMembers(roomId, rawMembersEvents); + } + } }; /** From 0364af73378ab6743348dc0614c20e0683c897f6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 30 Jul 2018 15:24:43 +0200 Subject: [PATCH 096/472] update indexeddb store to store member events, not profile information --- src/store/indexeddb-local-backend.js | 36 ++++++++++------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index 7d64c4d6d..e9d637eff 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -34,8 +34,8 @@ function createDatabase(db) { } function upgradeSchemaV2(db) { - const llMembersStore = db.createObjectStore("lazy_loaded_members", { keyPath: ["roomId", "userId"]}); - llMembersStore.createIndex("room", "roomId"); + const oobMembersStore = db.createObjectStore("oob_membership_events", { keyPath: ["room_id", "state_key"]}); + oobMembersStore.createIndex("room", "room_id"); } /** @@ -197,25 +197,20 @@ LocalIndexedDBStoreBackend.prototype = { getOutOfBandMembers: function(roomId) { return new Promise((resolve, reject) =>{ - const tx = this.db.transaction(["lazy_loaded_members"], "readonly"); - const store = tx.objectStore("lazy_loaded_members"); + 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 members = []; + const membershipEvents = []; request.onsuccess = (event) => { const cursor = event.target.result; if (!cursor) { - return resolve(members); + return resolve(membershipEvents); } const record = cursor.value; - members.push({ - userId: record.userId, - displayName: record.displayName, - membership: record.membership, - avatarUrl: record.avatarUrl, - }); + membershipEvents.push(record); cursor.continue(); }; request.onerror = (err) => { @@ -224,21 +219,14 @@ LocalIndexedDBStoreBackend.prototype = { }); }, - setOutOfBandMembers: function(roomId, members) { + setOutOfBandMembers: function(roomId, membershipEvents) { function ignoreResult() {}; // run everything in a promise so anything that throws will reject return new Promise((resolve) =>{ - const tx = this.db.transaction(["lazy_loaded_members"], "readwrite"); - const store = tx.objectStore("lazy_loaded_members"); - const puts = members.map((m) => { - const record = { - roomId: roomId, - userId: m.userId, - displayName: m.displayName, - avatarUrl: m.avatarUrl, - membership: m.membership - }; - let putPromise = promiseifyRequest(store.put(record)); + const tx = this.db.transaction(["oob_membership_events"], "readwrite"); + const store = tx.objectStore("oob_membership_events"); + const puts = membershipEvents.map((e) => { + let putPromise = promiseifyRequest(store.put(e)); return putPromise.then(ignoreResult); }); resolve(Promise.all(puts).then(ignoreResult)); From 86105611fca7fe9815f35147cf02116f328066cc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 30 Jul 2018 17:17:50 +0200 Subject: [PATCH 097/472] we dont need a separate index? --- src/client.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/client.js b/src/client.js index 8105ea384..eade585a7 100644 --- a/src/client.js +++ b/src/client.js @@ -767,8 +767,6 @@ MatrixClient.prototype._loadMembers = async function(room) { const lastEventId = room.getLastEventId(); const response = await this.members(roomId, "join", "leave", lastEventId); rawMembersEvents = response.chunk; - // TODO don't block on writing - await this.store.setOutOfBandMembers(roomId, rawMembersEvents); } const memberEvents = rawMembersEvents.map(this.getEventMapper()); return {memberEvents, fromServer}; From a8c73f7a4dfbe2e4e7b3594ca3665be3c0392a49 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Jul 2018 16:10:53 +0200 Subject: [PATCH 098/472] add logging, should be useful as long as not merged into develop --- src/client.js | 5 +++++ src/models/room-state.js | 2 ++ src/store/indexeddb-local-backend.js | 6 ++++++ 3 files changed, 13 insertions(+) diff --git a/src/client.js b/src/client.js index eade585a7..977755734 100644 --- a/src/client.js +++ b/src/client.js @@ -767,6 +767,7 @@ MatrixClient.prototype._loadMembers = async function(room) { const lastEventId = room.getLastEventId(); const response = await this.members(roomId, "join", "leave", lastEventId); rawMembersEvents = response.chunk; + console.log(`LL: got ${rawMembersEvents.length} members from server`); } const memberEvents = rawMembersEvents.map(this.getEventMapper()); return {memberEvents, fromServer}; @@ -807,8 +808,12 @@ MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { // scenario for DM's where all the members would likely // be known without lazy loading. if (rawMembersEvents.length) { + console.log(`LL: telling backend to store ${rawMembersEvents.length} members`); await this.store.setOutOfBandMembers(roomId, rawMembersEvents); } + else { + console.log(`LL: no members needed to be stored`); + } } }; diff --git a/src/models/room-state.js b/src/models/room-state.js index 698b894b4..57716194a 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -432,9 +432,11 @@ RoomState.prototype.markOutOfBandMembersFailed = function() { * @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)); }; diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index e9d637eff..1cca9f3e2 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -216,10 +216,14 @@ LocalIndexedDBStoreBackend.prototype = { request.onerror = (err) => { reject(err); }; + }).then((membershipEvents) => { + console.log(`LL: got ${membershipEvents.length} membershipEvents from storage for room ${roomId} ...`); + return membershipEvents; }); }, setOutOfBandMembers: function(roomId, membershipEvents) { + console.log(`LL: backend about to store ${membershipEvents.length} members for ${roomId}`); function ignoreResult() {}; // run everything in a promise so anything that throws will reject return new Promise((resolve) =>{ @@ -230,6 +234,8 @@ LocalIndexedDBStoreBackend.prototype = { return putPromise.then(ignoreResult); }); resolve(Promise.all(puts).then(ignoreResult)); + }).then(() => { + console.log(`LL: backend done storing for ${roomId}!`); }); }, From 5e11bf735e498f093812e472ef8dd2cda4cebfaa Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Jul 2018 16:56:18 +0200 Subject: [PATCH 099/472] 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. --- src/client.js | 16 ++----- src/store/indexeddb-local-backend.js | 67 +++++++++++++++++++++++---- src/store/indexeddb-remote-backend.js | 19 +++++++- src/store/indexeddb.js | 19 +++++++- 4 files changed, 94 insertions(+), 27 deletions(-) diff --git a/src/client.js b/src/client.js index 977755734..9365b54a0 100644 --- a/src/client.js +++ b/src/client.js @@ -762,7 +762,7 @@ MatrixClient.prototype._loadMembers = async function(room) { // were the members loaded from the server? let fromServer = false; let rawMembersEvents = await this.store.getOutOfBandMembers(roomId); - if (rawMembersEvents.length == 0) { + if (rawMembersEvents === null) { fromServer = true; const lastEventId = room.getLastEventId(); const response = await this.members(roomId, "join", "leave", lastEventId); @@ -802,18 +802,8 @@ MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { const rawMembersEvents = room.currentState.getMembers() .filter((m) => m.isOutOfBand()) .map((m) => m.events.member.event); - // TODO: probably need a way to mark a room as lazy loaded - // even though we didn't store any members, as we'll just - // lazy loaded the room in every session. This is a likely - // scenario for DM's where all the members would likely - // be known without lazy loading. - if (rawMembersEvents.length) { - console.log(`LL: telling backend to store ${rawMembersEvents.length} members`); - await this.store.setOutOfBandMembers(roomId, rawMembersEvents); - } - else { - console.log(`LL: no members needed to be stored`); - } + console.log(`LL: telling backend to store ${rawMembersEvents.length} members`); + await this.store.setOutOfBandMembers(roomId, rawMembersEvents); } }; diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index 1cca9f3e2..2b97e0553 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -34,7 +34,10 @@ function createDatabase(db) { } function upgradeSchemaV2(db) { - const oobMembersStore = db.createObjectStore("oob_membership_events", { keyPath: ["room_id", "state_key"]}); + const oobMembersStore = db.createObjectStore( + "oob_membership_events", { + keyPath: ["room_id", "state_key"], + }); oobMembersStore.createIndex("room", "room_id"); } @@ -195,6 +198,13 @@ LocalIndexedDBStoreBackend.prototype = { }); }, + /** + * 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 new Promise((resolve, reject) =>{ const tx = this.db.transaction(["oob_membership_events"], "readonly"); @@ -204,36 +214,73 @@ LocalIndexedDBStoreBackend.prototype = { 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; - membershipEvents.push(record); + if (record.oob_written) { + oobWritten = true; + } else { + membershipEvents.push(record); + } cursor.continue(); }; request.onerror = (err) => { reject(err); }; - }).then((membershipEvents) => { - console.log(`LL: got ${membershipEvents.length} membershipEvents from storage for room ${roomId} ...`); - return membershipEvents; + }).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 + * @returns {Promise} when all members have been stored + */ setOutOfBandMembers: function(roomId, membershipEvents) { - console.log(`LL: backend about to store ${membershipEvents.length} members for ${roomId}`); - function ignoreResult() {}; + console.log(`LL: backend about to store ${membershipEvents.length}` + + ` members for ${roomId}`); + function ignoreResult() {} // run everything in a promise so anything that throws will reject return new Promise((resolve) =>{ const tx = this.db.transaction(["oob_membership_events"], "readwrite"); const store = tx.objectStore("oob_membership_events"); - const puts = membershipEvents.map((e) => { - let putPromise = promiseifyRequest(store.put(e)); + const eventPuts = membershipEvents.map((e) => { + const putPromise = promiseifyRequest(store.put(e)); return putPromise.then(ignoreResult); }); - resolve(Promise.all(puts).then(ignoreResult)); + // 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, + }; + const markerPut = promiseifyRequest(store.put(markerObject)); + const allPuts = eventPuts.concat(markerPut); + resolve(Promise.all(allPuts).then(ignoreResult)); }).then(() => { console.log(`LL: backend done storing for ${roomId}!`); }); diff --git a/src/store/indexeddb-remote-backend.js b/src/store/indexeddb-remote-backend.js index 1594eef26..7a58ded1b 100644 --- a/src/store/indexeddb-remote-backend.js +++ b/src/store/indexeddb-remote-backend.js @@ -87,12 +87,27 @@ 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]); }, - setOutOfBandMembers: function(roomId, members) { - return this._doCmd('setOutOfBandMembers', [roomId, members]); + /** + * 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]); }, /** diff --git a/src/store/indexeddb.js b/src/store/indexeddb.js index 0086eb531..5e027537e 100644 --- a/src/store/indexeddb.js +++ b/src/store/indexeddb.js @@ -219,12 +219,27 @@ 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); }; -IndexedDBStore.prototype.setOutOfBandMembers = function(roomId, members) { - return this.backend.setOutOfBandMembers(roomId, members); +/** + * 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); }; module.exports.IndexedDBStore = IndexedDBStore; From 977b9eb686e3ce6c901c494a64f7bf2680b91b61 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Jul 2018 17:15:47 +0200 Subject: [PATCH 100/472] implement memory store methods --- src/store/memory.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/store/memory.js b/src/store/memory.js index 4f2f7c754..a5f72af4f 100644 --- a/src/store/memory.js +++ b/src/store/memory.js @@ -52,6 +52,9 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) { // type : content }; this.localStorage = opts.localStorage; + this._oobMembers = { + // roomId: [member events] + }; }; module.exports.MatrixInMemoryStore.prototype = { @@ -377,4 +380,26 @@ 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(); + }, }; From b034f67a0f3f93ad768441879c93c721743a985f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 31 Jul 2018 18:08:59 +0200 Subject: [PATCH 101/472] add oob member methods to stub store --- src/store/stub.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/store/stub.js b/src/store/stub.js index f41e0c5e0..9c628b287 100644 --- a/src/store/stub.js +++ b/src/store/stub.js @@ -264,6 +264,14 @@ StubStore.prototype = { deleteAllData: function() { return Promise.resolve(); }, + + getOutOfBandMembers: function() { + return Promise.resolve(null); + }, + + setOutOfBandMembers: function() { + return Promise.resolve(); + }, }; /** Stub Store class. */ From c0f706a2a24a0625c2f3f93d8587aacaca1e16f6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 2 Aug 2018 11:10:04 +0200 Subject: [PATCH 102/472] move userId into room --- spec/integ/matrix-client-methods.spec.js | 2 +- spec/unit/room.spec.js | 91 ++++++++++++++---------- src/models/room.js | 21 +++--- src/sync.js | 14 ++-- 4 files changed, 71 insertions(+), 57 deletions(-) diff --git a/spec/integ/matrix-client-methods.spec.js b/spec/integ/matrix-client-methods.spec.js index 57a63a842..5077a8ef4 100644 --- a/spec/integ/matrix-client-methods.spec.js +++ b/spec/integ/matrix-client-methods.spec.js @@ -159,7 +159,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, diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 3a436342d..291e49a37 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -387,7 +387,7 @@ describe("Room", function() { let events = null; beforeEach(function() { - room = new Room(roomId, {timelineSupport: timelineSupport}); + room = new Room(roomId, null, {timelineSupport: timelineSupport}); // set events each time to avoid resusing Event objects (which // doesn't work because they get frozen) events = [ @@ -469,7 +469,7 @@ describe("Room", function() { describe("compareEventOrdering", function() { beforeEach(function() { - room = new Room(roomId, {timelineSupport: true}); + room = new Room(roomId, null, {timelineSupport: true}); }); const events = [ @@ -658,7 +658,7 @@ describe("Room", function() { beforeEach(function() { // no mocking - room = new Room(roomId); + room = new Room(roomId, userA); }); describe("Room.recalculate => Stripped State Events", function() { @@ -677,7 +677,7 @@ describe("Room", function() { }, ]; - room.recalculate(userA); + room.recalculate(); expect(room.name).toEqual(roomName); }); @@ -696,7 +696,7 @@ describe("Room", function() { }, ]; - room.recalculate(userA); + room.recalculate(); expect(room.name).toEqual(roomName); }); }); @@ -711,7 +711,7 @@ describe("Room", function() { "m.heroes": [userB, userC, userD], }); - room.recalculate(userA); + room.recalculate(); expect(room.name).toEqual(`${userB} and 2 others`); }); @@ -721,7 +721,7 @@ describe("Room", function() { "m.joined_member_count": 2, }); - room.recalculate(userA); + room.recalculate(); expect(room.name).toEqual(userB); }); @@ -733,7 +733,7 @@ describe("Room", function() { "m.heroes": [userB], }); - room.recalculate(userA); + room.recalculate(); expect(room.name).toEqual(name); }); @@ -745,7 +745,7 @@ describe("Room", function() { "m.joined_member_count": 50, "m.invited_member_count": 50, }); - room.recalculate(userA); + room.recalculate(); expect(room.name).toEqual(`${name} and 98 others`); }); @@ -757,7 +757,7 @@ describe("Room", function() { room.setSummary({ "m.heroes": [userB, userC], }); - room.recalculate(userA); + room.recalculate(); expect(room.name).toEqual(`${nameB} and ${nameC}`); }); @@ -768,7 +768,7 @@ describe("Room", function() { room.setSummary({ "m.heroes": [userB], }); - room.recalculate(userA); + room.recalculate(); expect(room.name).toEqual(nameB); }); @@ -777,7 +777,7 @@ describe("Room", function() { "m.heroes": [], "m.invited_member_count": 1, }); - room.recalculate(userA); + room.recalculate(); expect(room.name).toEqual("Empty room"); }); }); @@ -791,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]; @@ -812,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); @@ -825,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); @@ -837,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); }); @@ -848,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); }); @@ -858,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); }); @@ -868,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); }); @@ -878,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); }); @@ -888,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); }); @@ -898,7 +898,7 @@ describe("Room", function() { const roomName = "A mighty name indeed"; setJoinRule("public"); setRoomName(roomName); - room.recalculate(userA); + room.recalculate(); expect(room.name).toEqual(roomName); }); @@ -906,7 +906,7 @@ describe("Room", function() { " a room name and alias don't exist and it is a self-chat.", function() { setJoinRule("invite"); addMember(userA); - room.recalculate(userA); + room.recalculate(); expect(room.name).toEqual("Empty room"); }); @@ -914,7 +914,7 @@ describe("Room", function() { " 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"); }); @@ -922,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"); }); @@ -931,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"); }); @@ -941,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); }); }); }); @@ -1192,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, userA, { pendingEventOrdering: "detached", }); const eventA = utils.mkMessage({ @@ -1218,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, userA, { pendingEventOrdering: "chronological", }); const eventA = utils.mkMessage({ @@ -1242,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, userA, { pendingEventOrdering: "detached", }); const eventA = utils.mkMessage({ @@ -1278,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, userA); const eventA = utils.mkMessage({ room: roomId, user: userA, event: true, }); @@ -1318,7 +1318,7 @@ describe("Room", function() { }); it("should apply member events", async function() { - const room = new Room(roomId); + const room = new Room(roomId, null); await room.loadOutOfBandMembers(Promise.resolve([memberEvent])); const memberA = room.getMember("@user_a:bar"); expect(memberA.name).toEqual("User A"); @@ -1329,7 +1329,7 @@ describe("Room", function() { user: "@user_a:bar", mship: "join", room: roomId, event: true, name: "Ms A", }); - const room = new Room(roomId); + const room = new Room(roomId, null); const promise2 = Promise.resolve([memberEvent2]); const promise1 = promise2.then(() => [memberEvent]); @@ -1342,7 +1342,7 @@ describe("Room", function() { }); it("should revert needs loading on error", async function() { - const room = new Room(roomId); + const room = new Room(roomId, null); let hasThrown = false; try { await room.loadOutOfBandMembers(Promise.reject(new Error("bugger"))); @@ -1353,4 +1353,17 @@ describe("Room", function() { expect(room.needsOutOfBandMembers()).toEqual(true); }); }); + + describe("getMyMembership", function() { + it("should return synced membership if membership isn't available yet", async function() { + const room = new Room(roomId, userA); + room.setSyncedMembership("invite"); + expect(room.getMyMembership()).toEqual("invite"); + room.addLiveEvents([utils.mkMembership({ + user: userA, mship: "join", + room: roomId, event: true, + })]); + expect(room.getMyMembership()).toEqual("join"); + }); + }); }); diff --git a/src/models/room.js b/src/models/room.js index 0d0abc71f..6301501af 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -68,6 +68,7 @@ function synthesizeReceipt(userId, event, receiptType) { * @constructor * @alias module:models/room * @param {string} roomId Required. The ID of this room. + * @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 @@ -102,7 +103,7 @@ function synthesizeReceipt(userId, event, receiptType) { * @prop {*} storageToken A token which a data store can use to remember * the state of the room. */ -function Room(roomId, opts) { +function Room(roomId, myUserId, opts) { opts = opts || {}; opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological"; @@ -115,6 +116,7 @@ function Room(roomId, opts) { ); } + this.myUserId = myUserId; this.roomId = roomId; this.name = roomId; this.tags = { @@ -228,9 +230,9 @@ Room.prototype.getLastEventId = function() { * @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(myUserId) { - if (myUserId) { - const me = this.getMember(myUserId); +Room.prototype.getMyMembership = function() { + if (this.myUserId) { + const me = this.getMember(this.myUserId); if (me) { return me.membership; } @@ -1014,15 +1016,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 || []; @@ -1038,14 +1039,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, }); @@ -1320,7 +1321,7 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { return memberNamesToRoomName(otherNames, inviteJoinCount); } - const myMembership = room.getMyMembership(userId); + const myMembership = room.getMyMembership(); // if I have created a room and invited people throuh // 3rd party invites if (myMembership == 'join') { diff --git a/src/sync.js b/src/sync.js index d2f6dc760..2c617b8a5 100644 --- a/src/sync.js +++ b/src/sync.js @@ -113,7 +113,7 @@ function SyncApi(client, opts) { */ SyncApi.prototype.createRoom = function(roomId) { const client = this.client; - const room = new Room(roomId, { + const room = new Room(roomId, client.getUserId(), { pendingEventOrdering: this.opts.pendingEventOrdering, timelineSupport: client.timelineSupport, }); @@ -232,7 +232,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); @@ -303,7 +303,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 @@ -969,7 +969,7 @@ SyncApi.prototype._processSyncResponse = async function( self._mapSyncEventsFormat(inviteObj.invite_state, room); self._processRoomEvents(room, stateEvents); if (inviteObj.isBrandNewRoom) { - room.recalculate(client.credentials.userId); + room.recalculate(); client.store.storeRoom(room); client.emit("Room", room); } @@ -1076,7 +1076,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); @@ -1116,7 +1116,7 @@ SyncApi.prototype._processSyncResponse = async function( 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); @@ -1406,7 +1406,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 From f55a2079bf23465721439c031027b60fa8e3fb8a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 2 Aug 2018 11:37:43 +0200 Subject: [PATCH 103/472] replace getMember(myId).membership with getMyMembership This works with rooms which haven't had their members loaded yet. --- examples/node/app.js | 6 +++--- src/crypto/index.js | 10 ++-------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/examples/node/app.js b/examples/node/app.js index dcff22aac..fd486830b 100644 --- a/examples/node/app.js +++ b/examples/node/app.js @@ -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( diff --git a/src/crypto/index.js b/src/crypto/index.js index 4a9cbf7eb..273dcce70 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1008,14 +1008,8 @@ Crypto.prototype._getE2eRooms = function() { } // ignore any rooms which we have left - const me = room.getMember(this._userId); - if (!me || ( - me.membership !== "join" && me.membership !== "invite" - )) { - return false; - } - - return true; + const myMembership = room.getMyMembership(); + return myMembership === "join" || myMembership === "invite"; }); }; From a8d0d8f33dae99dc5422dc34bd458e63d54e77d6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 2 Aug 2018 11:38:58 +0200 Subject: [PATCH 104/472] provide method on room to help with DM detection with fallback to summary heroes/counts. --- src/models/room.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/models/room.js b/src/models/room.js index 6301501af..8673ee5e9 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -240,6 +240,21 @@ Room.prototype.getMyMembership = function() { return this._syncedMembership; }; +Room.prototype.getDMInviter = function() { + if (this.myUserId) { + const me = this.getMember(this.myUserId); + if (me) { + return me.getDMInviter(); + } + } + // fall back to summary information + const memberCount = room.currentState.getJoinedMemberCount() + + room.currentState.getInvitedMemberCount(); + if (memberCount == 2 && this._summaryHeroes.length) { + return this._summaryHeroes[0]; + } +} + /** * Sets the membership this room was received as during sync * @param {string} membership join | leave | invite From 2862b490571c555121113f0050ec6426d3523d61 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 2 Aug 2018 12:14:33 +0200 Subject: [PATCH 105/472] Only return hero in getDMInviter if we were invited --- src/models/room.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 8673ee5e9..cfa1a8ea7 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -240,6 +240,11 @@ Room.prototype.getMyMembership = function() { return this._syncedMembership; }; +/** + * 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); @@ -247,11 +252,13 @@ Room.prototype.getDMInviter = function() { return me.getDMInviter(); } } - // fall back to summary information - const memberCount = room.currentState.getJoinedMemberCount() + - room.currentState.getInvitedMemberCount(); - if (memberCount == 2 && this._summaryHeroes.length) { - return this._summaryHeroes[0]; + if (this._syncedMembership === "invite") { + // fall back to summary information + const memberCount = this.currentState.getJoinedMemberCount() + + this.currentState.getInvitedMemberCount(); + if (memberCount == 2 && this._summaryHeroes.length) { + return this._summaryHeroes[0]; + } } } From f261599435d7d565df4a2a8a7db582a35a1b58df Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 2 Aug 2018 12:14:56 +0200 Subject: [PATCH 106/472] fix lint --- spec/unit/room.spec.js | 3 ++- src/models/room.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 291e49a37..2cc3bfe47 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1355,7 +1355,8 @@ describe("Room", function() { }); describe("getMyMembership", function() { - it("should return synced membership if membership isn't available yet", async function() { + it("should return synced membership if membership isn't available yet", + async function() { const room = new Room(roomId, userA); room.setSyncedMembership("invite"); expect(room.getMyMembership()).toEqual("invite"); diff --git a/src/models/room.js b/src/models/room.js index cfa1a8ea7..ffcce4e60 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -260,7 +260,7 @@ Room.prototype.getDMInviter = function() { return this._summaryHeroes[0]; } } -} +}; /** * Sets the membership this room was received as during sync From 1dc4b8bb63011ef266727d6f1a5664b00588c706 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 1 Aug 2018 15:44:59 +0200 Subject: [PATCH 107/472] 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 --- src/client.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client.js b/src/client.js index 9365b54a0..0d9b5b495 100644 --- a/src/client.js +++ b/src/client.js @@ -3084,6 +3084,8 @@ MatrixClient.prototype.getTurnServers = function() { * * @param {Boolean=} opts.disablePresence True to perform syncing without automatically * updating presence. + * @param {Boolean=} opts.lazyLoadMembers True to not load all membership events during + * initial sync but fetch them when needed by calling `loadOutOfBandMembers` */ MatrixClient.prototype.startClient = function(opts) { if (this.clientRunning) { From 157be6da05421432907fb4132b662a51454c595a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 1 Aug 2018 15:47:26 +0200 Subject: [PATCH 108/472] centralize creating a /messages request so we only need to add LL filter once --- src/client.js | 80 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/src/client.js b/src/client.js index 0d9b5b495..5b5b40e1a 100644 --- a/src/client.js +++ b/src/client.js @@ -1984,14 +1984,6 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) { // reduce the required number of events appropriately limit = limit - numAdded; - const path = utils.encodeUri( - "/rooms/$roomId/messages", {$roomId: room.roomId}, - ); - const params = { - from: room.oldState.paginationToken, - limit: limit, - dir: 'b', - }; const defer = Promise.defer(); info = { promise: defer.promise, @@ -2001,7 +1993,11 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) { // wait for a time before doing this request // (which may be 0 in order not to special case the code paths) Promise.delay(timeToWaitMs).then(function() { - return self._http.authedRequest(callback, "GET", path, params); + return self._createMessagesRequest( + room.roomId, + room.oldState.paginationToken, + limit, + 'b'); }).done(function(res) { const matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self)); room.addEventsToTimeline(matrixEvents, true, room.getLiveTimeline()); @@ -2062,11 +2058,16 @@ MatrixClient.prototype.paginateEventContext = function(eventContext, opts) { limit: ('limit' in opts) ? opts.limit : 30, dir: dir, }; + if (this._clientOpts.filter) { + params.filter = JSON.stringify({lazy_load_members: true}); + } + + const roomId = eventContext.getEvent().getRoomId(); + const promise = this._createMessagesRequest( + roomId, token, opts.limit, dir); const self = this; - const promise = - self._http.authedRequest(undefined, "GET", path, params, - ).then(function(res) { + promise.then(function(res) { let token = res.end; if (res.chunk.length === 0) { token = null; @@ -2167,6 +2168,35 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) { return promise; }; +MatrixClient.prototype._createMessagesRequest = function(roomId, fromToken, limit, dir, timelineFilter = undefined) { + const path = utils.encodeUri( + "/rooms/$roomId/messages", {$roomId: roomId}, + ); + if (limit === undefined) { + limit = 30; + } + const params = { + from: fromToken, + limit: limit, + dir: dir, + }; + + let filter = null; + if (this._clientOpts.lazyLoadMembers) { + filter = {lazy_load_members: true}; + } + if (timelineFilter) { + // XXX: it's horrific that /messages' filter parameter doesn't match + // /sync's one - see https://matrix.org/jira/browse/SPEC-451 + filter = filter || {}; + Object.assign(filter, timelineFilter.getRoomTimelineFilterComponent()); + } + if (filter) { + params.filter = JSON.stringify(filter); + } + return this._http.authedRequest(undefined, "GET", path, params); +} + /** * Take an EventTimeline, and back/forward-fill results. @@ -2261,25 +2291,13 @@ MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) { throw new Error("Unknown room " + eventTimeline.getRoomId()); } - path = utils.encodeUri( - "/rooms/$roomId/messages", {$roomId: eventTimeline.getRoomId()}, - ); - params = { - from: token, - limit: ('limit' in opts) ? opts.limit : 30, - dir: dir, - }; - - const filter = eventTimeline.getFilter(); - if (filter) { - // XXX: it's horrific that /messages' filter parameter doesn't match - // /sync's one - see https://matrix.org/jira/browse/SPEC-451 - params.filter = JSON.stringify(filter.getRoomTimelineFilterComponent()); - } - - promise = - this._http.authedRequest(undefined, "GET", path, params, - ).then(function(res) { + promise = this._createMessagesRequest( + eventTimeline.getRoomId(), + token, + opts.limit, + dir, + eventTimeline.getFilter()); + promise.then(function(res) { const token = res.end; const matrixEvents = utils.map(res.chunk, self.getEventMapper()); eventTimeline.getTimelineSet() From 769d5113f77e0cfd6007fa840408f033e58e6edc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 1 Aug 2018 17:24:36 +0200 Subject: [PATCH 109/472] prepend state from /messages to appropriate timeline/room state (excluding event context for now) --- src/client.js | 10 ++++++++++ src/models/room-state.js | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/src/client.js b/src/client.js index 5b5b40e1a..dc75744f7 100644 --- a/src/client.js +++ b/src/client.js @@ -2000,6 +2000,10 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) { 'b'); }).done(function(res) { const matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self)); + if (res.state) { + const stateEvents = utils.map(res.state, _PojoToMatrixEventMapper(self)); + room.currentState.prependStateEvents(stateEvents); + } room.addEventsToTimeline(matrixEvents, true, room.getLiveTimeline()); room.oldState.paginationToken = res.end; if (res.chunk.length === 0) { @@ -2068,6 +2072,7 @@ MatrixClient.prototype.paginateEventContext = function(eventContext, opts) { const self = this; promise.then(function(res) { + // TODO: no RoomState on EventContext, where should the members go? let token = res.end; if (res.chunk.length === 0) { token = null; @@ -2298,6 +2303,11 @@ MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) { dir, eventTimeline.getFilter()); promise.then(function(res) { + if (res.state) { + const roomState = eventTimeline.getState(dir); + const stateEvents = utils.map(res.state, self.getEventMapper()); + roomState.prependStateEvents(stateEvents); + } const token = res.end; const matrixEvents = utils.map(res.chunk, self.getEventMapper()); eventTimeline.getTimelineSet() diff --git a/src/models/room-state.js b/src/models/room-state.js index 57716194a..34713075b 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -262,6 +262,15 @@ RoomState.prototype.clone = function() { return copy; }; + +RoomState.prototype.prependStateEvents = function(events) { + const unknownStateEvents = events.filter((event) => { + return this.events[event.getType()] === undefined || + this.events[event.getType()][event.getStateKey()] === undefined; + }); + + return this.setStateEvents(unknownStateEvents); +} /** * Add an array of one or more state MatrixEvents, overwriting * any existing state with the same {type, stateKey} tuple. Will fire From 748c4737f6a4cc0bf3b2c6f2e0884ebd0c0cbf03 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 2 Aug 2018 13:27:36 +0200 Subject: [PATCH 110/472] remove dead code --- src/client.js | 73 --------------------------------------------------- 1 file changed, 73 deletions(-) diff --git a/src/client.js b/src/client.js index dc75744f7..2361770ca 100644 --- a/src/client.js +++ b/src/client.js @@ -2022,79 +2022,6 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) { return defer.promise; }; -/** - * Take an EventContext, and back/forward-fill results. - * - * @param {module:models/event-context.EventContext} eventContext context - * object to be updated - * @param {Object} opts - * @param {boolean} opts.backwards true to fill backwards, false to go forwards - * @param {boolean} opts.limit number of events to request - * - * @return {module:client.Promise} Resolves: updated EventContext object - * @return {Error} Rejects: with an error response. - */ -MatrixClient.prototype.paginateEventContext = function(eventContext, opts) { - // TODO: we should implement a backoff (as per scrollback()) to deal more - // nicely with HTTP errors. - opts = opts || {}; - const backwards = opts.backwards || false; - - const token = eventContext.getPaginateToken(backwards); - if (!token) { - // no more results. - return Promise.reject(new Error("No paginate token")); - } - - const dir = backwards ? 'b' : 'f'; - const pendingRequest = eventContext._paginateRequests[dir]; - - if (pendingRequest) { - // already a request in progress - return the existing promise - return pendingRequest; - } - - const path = utils.encodeUri( - "/rooms/$roomId/messages", {$roomId: eventContext.getEvent().getRoomId()}, - ); - const params = { - from: token, - limit: ('limit' in opts) ? opts.limit : 30, - dir: dir, - }; - if (this._clientOpts.filter) { - params.filter = JSON.stringify({lazy_load_members: true}); - } - - const roomId = eventContext.getEvent().getRoomId(); - const promise = this._createMessagesRequest( - roomId, token, opts.limit, dir); - - const self = this; - promise.then(function(res) { - // TODO: no RoomState on EventContext, where should the members go? - let token = res.end; - if (res.chunk.length === 0) { - token = null; - } else { - const matrixEvents = utils.map(res.chunk, self.getEventMapper()); - if (backwards) { - // eventContext expects the events in timeline order, but - // back-pagination returns them in reverse order. - matrixEvents.reverse(); - } - eventContext.addEvents(matrixEvents, backwards); - } - eventContext.setPaginateToken(token, backwards); - return eventContext; - }).finally(function() { - eventContext._paginateRequests[dir] = null; - }); - eventContext._paginateRequests[dir] = promise; - - return promise; -}; - /** * Get an EventTimeline for the given event * From 2c0eb19a2711c827f4587a97902829462d2bbfcf Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 2 Aug 2018 16:53:53 +0200 Subject: [PATCH 111/472] 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. --- src/models/room-state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room-state.js b/src/models/room-state.js index 34713075b..48f2b68ba 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -180,7 +180,7 @@ RoomState.prototype.getSentinelMember = function(userId) { sentinel = new RoomMember(this.roomId, userId); const member = this.members[userId]; if (member) { - Object.assign(sentinel, member); + sentinel.setMembershipEvent(member.events.member); } this._sentinels[userId] = sentinel; } From bffc20612d9979b7a0a13bab6eeb7209343b4163 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 2 Aug 2018 16:55:26 +0200 Subject: [PATCH 112/472] Fix: member avatar was always forward looking applying itself all the way till the next member event when back paginating --- src/models/room-member.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room-member.js b/src/models/room-member.js index 71504d5b6..598cae398 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -312,7 +312,7 @@ RoomMember.prototype.getAvatarUrl = */ RoomMember.prototype.getMxcAvatarUrl = function() { if(this.events.member) { - return this.events.member.getContent().avatar_url; + return this.events.member.getDirectionalContent().avatar_url; } else if(this.user) { return this.user.avatarUrl; } From 52149ce74a15df358601bb9c8c532cf2b4b5d490 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 2 Aug 2018 19:37:06 +0200 Subject: [PATCH 113/472] 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 --- src/client.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/client.js b/src/client.js index 2361770ca..9d4e96c83 100644 --- a/src/client.js +++ b/src/client.js @@ -45,6 +45,18 @@ const ContentHelpers = require("./content-helpers"); import ReEmitter from './ReEmitter'; import RoomList from './crypto/RoomList'; + +const LAZY_LOADING_MESSAGES_FILTER = { + lazy_load_members: true, +}; + +const LAZY_LOADING_SYNC_FILTER = { + room: { + state: LAZY_LOADING_MESSAGES_FILTER, + }, +}; + + const SCROLLBACK_DELAY_MS = 3000; let CRYPTO_ENABLED = false; @@ -2115,7 +2127,7 @@ MatrixClient.prototype._createMessagesRequest = function(roomId, fromToken, limi let filter = null; if (this._clientOpts.lazyLoadMembers) { - filter = {lazy_load_members: true}; + filter = LAZY_LOADING_MESSAGES_FILTER; } if (timelineFilter) { // XXX: it's horrific that /messages' filter parameter doesn't match @@ -3041,8 +3053,9 @@ MatrixClient.prototype.getTurnServers = function() { * updating presence. * @param {Boolean=} opts.lazyLoadMembers True to not load all membership events during * initial sync but fetch them when needed by calling `loadOutOfBandMembers` + * This will override the filter option at this moment. */ -MatrixClient.prototype.startClient = function(opts) { +MatrixClient.prototype.startClient = async function(opts) { if (this.clientRunning) { // client is already running. return; @@ -3072,6 +3085,10 @@ MatrixClient.prototype.startClient = function(opts) { // shallow-copy the opts dict before modifying and storing it opts = Object.assign({}, opts); + if (opts.lazyLoadMembers) { + opts.filter = await this.createFilter(LAZY_LOADING_SYNC_FILTER); + } + opts.crypto = this._crypto; opts.canResetEntireTimeline = (roomId) => { if (!this._canResetTimelineCallback) { From 01d87308509f80acf6a9c9ed99456cdeaeb93c93 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 2 Aug 2018 19:49:24 +0200 Subject: [PATCH 114/472] cleanup, lint and docs --- src/client.js | 18 +++++++++++++++--- src/models/room-state.js | 14 +++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/client.js b/src/client.js index 9d4e96c83..351eba3e5 100644 --- a/src/client.js +++ b/src/client.js @@ -2112,7 +2112,20 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) { return promise; }; -MatrixClient.prototype._createMessagesRequest = function(roomId, fromToken, limit, dir, timelineFilter = undefined) { +/** + * Makes a request to /messages with the appropriate lazy loading filter set. + * XXX: if we do get rid of scrollback (as it's not used at the moment), + * we could inline this method again in paginateEventTimeline as that would + * then be the only call-site + * @param {string} roomId + * @param {string} fromToken + * @param {number} limit the maximum amount of events the retrieve + * @param {string} dir 'f' or 'b' + * @param {Filter} timelineFilter the timeline filter to pass + * @return {Promise} + */ +MatrixClient.prototype._createMessagesRequest = +function(roomId, fromToken, limit, dir, timelineFilter = undefined) { const path = utils.encodeUri( "/rooms/$roomId/messages", {$roomId: roomId}, ); @@ -2139,8 +2152,7 @@ MatrixClient.prototype._createMessagesRequest = function(roomId, fromToken, limi params.filter = JSON.stringify(filter); } return this._http.authedRequest(undefined, "GET", path, params); -} - +}; /** * Take an EventTimeline, and back/forward-fill results. diff --git a/src/models/room-state.js b/src/models/room-state.js index 48f2b68ba..cbabacc1c 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -262,15 +262,23 @@ RoomState.prototype.clone = function() { return copy; }; - +/** + * Add previously unknown state events just before + * prepending events to the timeline. + * 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.prependStateEvents = function(events) { const unknownStateEvents = events.filter((event) => { return this.events[event.getType()] === undefined || this.events[event.getType()][event.getStateKey()] === undefined; }); - return this.setStateEvents(unknownStateEvents); -} + this.setStateEvents(unknownStateEvents); +}; + /** * Add an array of one or more state MatrixEvents, overwriting * any existing state with the same {type, stateKey} tuple. Will fire From 0fa49bc2cd4f2a2228ad81b1e1b5815b8b4b0bd6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 3 Aug 2018 13:54:28 +0200 Subject: [PATCH 115/472] PR feedback --- src/client.js | 14 +++++++------- src/store/indexeddb-local-backend.js | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/client.js b/src/client.js index 351eba3e5..82c6df904 100644 --- a/src/client.js +++ b/src/client.js @@ -795,17 +795,17 @@ MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { if (!room || !room.needsOutOfBandMembers()) { return; } + // intercept whether we need to store oob members afterwards + let membersNeedStoring = false; // Note that we don't await _loadMembers here first. // setLazyLoadedMembers sets a flag before it awaits the promise passed in // to avoid a race when calling membersNeedLoading/loadOutOfBandMembers // in fast succession, before the first promise resolves. - let membersPromise = this._loadMembers(room); - // intercept whether we need to store oob members afterwards - let membersNeedStoring = false; - membersPromise = membersPromise.then(({memberEvents, fromServer}) => { - membersNeedStoring = fromServer; - return memberEvents; - }); + const membersPromise = this._loadMembers(room) + .then(({memberEvents, fromServer}) => { + membersNeedStoring = fromServer; + return memberEvents; + }); await room.loadOutOfBandMembers(membersPromise); // if loadOutOfBandMembers throws, this wont be called // but that's fine as we don't want to store members diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index 2b97e0553..d7ca43431 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -202,7 +202,7 @@ LocalIndexedDBStoreBackend.prototype = { * 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 {Promise} 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) { From 5285b22a769642f9545d1f08c163620f9aa47362 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 3 Aug 2018 14:41:08 +0200 Subject: [PATCH 116/472] bump version as making startClient async is a breaking change --- CHANGELOG.md | 12 ++++++++++++ package.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4bf276ef..b1417a72b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +Changes in [0.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (TDB) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.6...v0.11.0) + + * 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 CHANGE +--------------- + + * `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. + 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) diff --git a/package.json b/package.json index 15eac9216..6f1ecfc14 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.10.7", + "version": "0.11.0", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From 7aa0dcc89f1e56d6017dbfc92593bf85e47831c9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 3 Aug 2018 14:44:51 +0200 Subject: [PATCH 117/472] PR feedback, rename method --- src/client.js | 2 +- src/models/room-state.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/client.js b/src/client.js index 82c6df904..1907cceaa 100644 --- a/src/client.js +++ b/src/client.js @@ -2014,7 +2014,7 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) { const matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self)); if (res.state) { const stateEvents = utils.map(res.state, _PojoToMatrixEventMapper(self)); - room.currentState.prependStateEvents(stateEvents); + room.currentState.setUnknownStateEvents(stateEvents); } room.addEventsToTimeline(matrixEvents, true, room.getLiveTimeline()); room.oldState.paginationToken = res.end; diff --git a/src/models/room-state.js b/src/models/room-state.js index cbabacc1c..fb049daa9 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -263,14 +263,13 @@ RoomState.prototype.clone = function() { }; /** - * Add previously unknown state events just before - * prepending events to the timeline. + * 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.prependStateEvents = function(events) { +RoomState.prototype.setUnknownStateEvents = function(events) { const unknownStateEvents = events.filter((event) => { return this.events[event.getType()] === undefined || this.events[event.getType()][event.getStateKey()] === undefined; From 8c01ed146985773ed0a3f29f4428072ed336832b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 3 Aug 2018 15:06:19 +0200 Subject: [PATCH 118/472] add comments explaining why we ignore the put promise result --- src/store/indexeddb-local-backend.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index d7ca43431..b24bcbdbe 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -266,6 +266,9 @@ LocalIndexedDBStoreBackend.prototype = { const store = tx.objectStore("oob_membership_events"); const eventPuts = membershipEvents.map((e) => { const putPromise = promiseifyRequest(store.put(e)); + // ignoring the result makes sure we discard the IDB success event + // ASAP, and not create a potentially big array containing them + // unneccesarily later on by calling Promise.all. return putPromise.then(ignoreResult); }); // aside from all the events, we also write a marker object to the store @@ -280,6 +283,9 @@ LocalIndexedDBStoreBackend.prototype = { }; const markerPut = promiseifyRequest(store.put(markerObject)); const allPuts = eventPuts.concat(markerPut); + // ignore the empty array Promise.all creates + // as this method should just resolve + // to undefined on success resolve(Promise.all(allPuts).then(ignoreResult)); }).then(() => { console.log(`LL: backend done storing for ${roomId}!`); From e3913bd397f99d6dec1a2bfd46a1bc9c5c9189c2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 3 Aug 2018 17:10:53 +0200 Subject: [PATCH 119/472] Fix: missed call site while renaming prependStateEvents --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 1907cceaa..b783536d1 100644 --- a/src/client.js +++ b/src/client.js @@ -2257,7 +2257,7 @@ MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) { if (res.state) { const roomState = eventTimeline.getState(dir); const stateEvents = utils.map(res.state, self.getEventMapper()); - roomState.prependStateEvents(stateEvents); + roomState.setUnknownStateEvents(stateEvents); } const token = res.end; const matrixEvents = utils.map(res.chunk, self.getEventMapper()); From 6609dfd4108c63229a781431f065a99be3821928 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 3 Aug 2018 15:48:53 +0200 Subject: [PATCH 120/472] initial support for lazy loading when calling /context --- src/client.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index b783536d1..fe3e4a213 100644 --- a/src/client.js +++ b/src/client.js @@ -2067,11 +2067,16 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) { }, ); + let params = undefined; + if (this._clientOpts.lazyLoadMembers) { + params = {filter: JSON.stringify(LAZY_LOADING_MESSAGES_FILTER)}; + } + // TODO: we should implement a backoff (as per scrollback()) to deal more // nicely with HTTP errors. const self = this; const promise = - self._http.authedRequest(undefined, "GET", path, + self._http.authedRequest(undefined, "GET", path, params, ).then(function(res) { if (!res.event) { throw new Error("'event' not in '/context' result - homeserver too old?"); @@ -2098,6 +2103,8 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) { timeline.initialiseState(utils.map(res.state, self.getEventMapper())); timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end; + } else { + timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(res.state); } timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start); From 3616a07dbb951bd51f2190041ebdfdedd0d01e90 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 6 Aug 2018 17:16:13 +0200 Subject: [PATCH 121/472] store /members promise on room while loading members --- src/models/room.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index ffcce4e60..7d6449fd1 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -175,6 +175,8 @@ function Room(roomId, myUserId, opts) { this._blacklistUnverifiedDevices = null; this._syncedMembership = null; this._summaryHeroes = null; + // awaited by getEncryptionTargetMembers while room mebers are loading + this._oobMembersPromise = null; } utils.inherits(Room, EventEmitter); @@ -283,19 +285,21 @@ Room.prototype.needsOutOfBandMembers = function() { * Loads the out-of-band members from the promise passed in * @param {Promise} eventsPromise promise that resolves to an array with membership MatrixEvents for the members */ -Room.prototype.loadOutOfBandMembers = async function(eventsPromise) { +Room.prototype.loadOutOfBandMembers = function(eventsPromise) { if (!this.needsOutOfBandMembers()) { - return; + return Promise.resolve(); } this.currentState.markOutOfBandMembersStarted(); - let events = null; - try { - events = await eventsPromise; - } catch (err) { + + // store the promise that already updated the room state + // to ensure that happens first + this._oobMembersPromise = eventsPromise.then((events) => { + this.currentState.setOutOfBandMembers(events); + }, (err) => { this.currentState.markOutOfBandMembersFailed(); throw err; //rethrow so calling code is aware operation failed - } - this.currentState.setOutOfBandMembers(events); + }); + return this._oobMembersPromise; }; /** From a8bf66d8af74ff0f237f2d172e9cf28109ec6502 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 6 Aug 2018 17:18:36 +0200 Subject: [PATCH 122/472] Make Room.getEncryptionTargetMembers async, as members might be loading --- src/crypto/algorithms/megolm.js | 38 ++++++++-------- src/crypto/algorithms/olm.js | 78 +++++++++++++++++---------------- src/crypto/index.js | 10 ++--- src/models/room.js | 9 ++-- 4 files changed, 70 insertions(+), 65 deletions(-) diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index d8a807092..a4eb32ab0 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -535,8 +535,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) { - const roomMembers = utils.map(room.getEncryptionTargetMembers(), function(u) { +MegolmEncryption.prototype._getDevicesInRoom = async function(room) { + const members = await room.getEncryptionTargetMembers(); + const roomMembers = utils.map(members, function(u) { return u.userId; }); @@ -555,29 +556,28 @@ MegolmEncryption.prototype._getDevicesInRoom = function(room) { // 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)) { + 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; }; /** diff --git a/src/crypto/algorithms/olm.js b/src/crypto/algorithms/olm.js index 26a4d90f8..69390b064 100644 --- a/src/crypto/algorithms/olm.js +++ b/src/crypto/algorithms/olm.js @@ -83,60 +83,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.getEncryptionTargetMembers(), 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); }; /** diff --git a/src/crypto/index.js b/src/crypto/index.js index 273dcce70..8b35bc0a4 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -652,7 +652,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic throw new Error(`Unable to enable encryption in unknown room ${roomId}`); } - const members = room.getEncryptionTargetMembers(); + const members = await room.getEncryptionTargetMembers(); members.forEach((m) => { this._deviceList.startTrackingDeviceList(m.userId); }); @@ -852,7 +852,7 @@ Crypto.prototype.handleDeviceListChanges = async function(syncData, syncDeviceLi // If we didn't make this assumption, we'd have to use the /keys/changes API // to get key changes between the sync token in the device list and the 'old' // sync token used here to make sure we didn't miss any. - this._evalDeviceListChanges(syncDeviceLists); + await this._evalDeviceListChanges(syncDeviceLists); }; /** @@ -968,7 +968,7 @@ Crypto.prototype._evalDeviceListChanges = async function(deviceLists) { // Check we really don't share any rooms with these users // any more: the server isn't required to give us the // exact correct set. - const e2eUserIds = new Set(this._getE2eUsers()); + const e2eUserIds = new Set(await this._getE2eUsers()); deviceLists.left.forEach((u) => { if (!e2eUserIds.has(u)) { @@ -983,10 +983,10 @@ Crypto.prototype._evalDeviceListChanges = async function(deviceLists) { * * @returns {string[]} List of user IDs */ -Crypto.prototype._getE2eUsers = function() { +Crypto.prototype._getE2eUsers = async function() { const e2eUserIds = []; for (const room of this._getE2eRooms()) { - const members = room.getEncryptionTargetMembers(); + const members = await room.getEncryptionTargetMembers(); for (const member of members) { e2eUserIds.push(member.userId); } diff --git a/src/models/room.js b/src/models/room.js index 7d6449fd1..ba0fc5b8c 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -569,10 +569,13 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline, /** * Get a list of members we should be encrypting for in this room - * @return {RoomMember[]} A list of members who we should encrypt messages for - * in this room. + * @return {Promise} A list of members who + * we should encrypt messages for in this room. */ - Room.prototype.getEncryptionTargetMembers = function() { + Room.prototype.getEncryptionTargetMembers = async function() { + if (this._oobMembersPromise) { + await _oobMembersPromise; + } let members = this.getMembersWithMembership("join"); if (this.shouldEncryptForInvitedMembers()) { members = members.concat(this.getMembersWithMembership("invite")); From 43a7a607b28c8e06aa0b0a97d35270a0c5efcdfb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 6 Aug 2018 17:48:50 +0200 Subject: [PATCH 123/472] fixup --- src/models/room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room.js b/src/models/room.js index ba0fc5b8c..7123f2416 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -574,7 +574,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline, */ Room.prototype.getEncryptionTargetMembers = async function() { if (this._oobMembersPromise) { - await _oobMembersPromise; + await this._oobMembersPromise; } let members = this.getMembersWithMembership("join"); if (this.shouldEncryptForInvitedMembers()) { From b4afe97289908d6be0de6e759160d1e043f03bf4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 6 Aug 2018 18:54:29 +0200 Subject: [PATCH 124/472] spelling --- src/models/room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room.js b/src/models/room.js index 7123f2416..0e056e7c8 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -175,7 +175,7 @@ function Room(roomId, myUserId, opts) { this._blacklistUnverifiedDevices = null; this._syncedMembership = null; this._summaryHeroes = null; - // awaited by getEncryptionTargetMembers while room mebers are loading + // awaited by getEncryptionTargetMembers while room members are loading this._oobMembersPromise = null; } From b14be026b794978edab049e826ae8588bc3e9939 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 6 Aug 2018 18:56:29 +0200 Subject: [PATCH 125/472] pass LL flag to room, to know if we should wait for lazy members at all --- src/models/room.js | 3 ++- src/sync.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/models/room.js b/src/models/room.js index 0e056e7c8..da506870b 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -278,7 +278,8 @@ Room.prototype.setSyncedMembership = function(membership) { * @return {bool} whether or not the members of this room need to be loaded */ Room.prototype.needsOutOfBandMembers = function() { - return this.currentState.needsOutOfBandMembers(); + return this._opts.lazyLoadMembers && + this.currentState.needsOutOfBandMembers(); }; /** diff --git a/src/sync.js b/src/sync.js index 2c617b8a5..fd893293e 100644 --- a/src/sync.js +++ b/src/sync.js @@ -114,6 +114,7 @@ function SyncApi(client, opts) { SyncApi.prototype.createRoom = function(roomId) { const client = this.client; const room = new Room(roomId, client.getUserId(), { + lazyLoadMembers: this.opts.lazyLoadMembers, pendingEventOrdering: this.opts.pendingEventOrdering, timelineSupport: client.timelineSupport, }); From 733a3ed102aee4c37570c008779f4c8323c5f11f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 6 Aug 2018 18:59:03 +0200 Subject: [PATCH 126/472] Defer encryption targeted users when OOB member loading hasn't started --- src/models/room.js | 13 +++++++++++-- src/utils.js | 9 +++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index da506870b..e1d3b9902 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -177,6 +177,9 @@ function Room(roomId, myUserId, opts) { this._summaryHeroes = null; // awaited by getEncryptionTargetMembers while room members are loading this._oobMembersPromise = null; + if (this._opts.lazyLoadMembers) { + this._oobMembersPromise = new utils.Deferred(); + } } utils.inherits(Room, EventEmitter); @@ -294,13 +297,19 @@ Room.prototype.loadOutOfBandMembers = function(eventsPromise) { // store the promise that already updated the room state // to ensure that happens first - this._oobMembersPromise = eventsPromise.then((events) => { + const updatedRoomStatePromise = eventsPromise.then((events) => { this.currentState.setOutOfBandMembers(events); }, (err) => { this.currentState.markOutOfBandMembersFailed(); throw err; //rethrow so calling code is aware operation failed }); - return this._oobMembersPromise; + // resolve the Deferred with the pending updated room promise, + // this will signal OOB members are now available to + // dependant code like getEncryptionTargetMembers + if (this._oobMembersPromise) { + this._oobMembersPromise.resolve(updatedRoomStatePromise); + } + return updatedRoomStatePromise; }; /** diff --git a/src/utils.js b/src/utils.js index 1587c64a3..20993052d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -672,3 +672,12 @@ module.exports.removeHiddenChars = function(str) { return str.normalize('NFD').replace(removeHiddenCharsRegex, ''); }; const removeHiddenCharsRegex = /[\u200B-\u200D\u0300-\u036f\uFEFF\s]/g; + +module.exports.Deferred = function Deferred() { + let resolve = null; + const promise = new Promise((r) => { + resolve = r; + }); + promise.resolve = resolve; + return promise; +}; From 5d92ec3b7ba0f4a93fb751c24aa27256771ee08f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 6 Aug 2018 19:00:08 +0200 Subject: [PATCH 127/472] 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. --- src/crypto/index.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 8b35bc0a4..9fab416f6 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -644,19 +644,20 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic }); this._roomEncryptors[roomId] = alg; - // make sure we are tracking the device lists for all users in this room. - console.log("Enabling encryption in " + roomId + "; " + - "starting to track device lists for all users therein"); - const room = this._clientStore.getRoom(roomId); - if (!room) { - throw new Error(`Unable to enable encryption in unknown room ${roomId}`); - } - const members = await room.getEncryptionTargetMembers(); - members.forEach((m) => { - this._deviceList.startTrackingDeviceList(m.userId); - }); if (!inhibitDeviceQuery) { + // make sure we are tracking the device lists for all users in this room. + console.log("Enabling encryption in " + roomId + "; " + + "starting to track device lists for all users therein"); + const room = this._clientStore.getRoom(roomId); + if (!room) { + throw new Error(`Unable to enable encryption in unknown room ${roomId}`); + } + + const members = await room.getEncryptionTargetMembers(); + members.forEach((m) => { + this._deviceList.startTrackingDeviceList(m.userId); + }); this._deviceList.refreshOutdatedDeviceLists(); } }; From 864ea749e5bfe2713a0520f3e2d34efbbd16a30a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 7 Aug 2018 13:08:41 +0200 Subject: [PATCH 128/472] Move /members fetching to room as getEncryptionTargetMembers needs it --- spec/unit/room.spec.js | 2 +- src/client.js | 50 ---------------------- src/models/room.js | 97 ++++++++++++++++++++++++++++++------------ src/sync.js | 2 +- 4 files changed, 72 insertions(+), 79 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 2cc3bfe47..274a7d19d 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -469,7 +469,7 @@ describe("Room", function() { describe("compareEventOrdering", function() { beforeEach(function() { - room = new Room(roomId, null, {timelineSupport: true}); + room = new Room(roomId, null, null, null, {timelineSupport: true}); }); const events = [ diff --git a/src/client.js b/src/client.js index fe3e4a213..40c491158 100644 --- a/src/client.js +++ b/src/client.js @@ -769,56 +769,6 @@ MatrixClient.prototype.getRoom = function(roomId) { return this.store.getRoom(roomId); }; -MatrixClient.prototype._loadMembers = async function(room) { - const roomId = room.roomId; - // were the members loaded from the server? - let fromServer = false; - let rawMembersEvents = await this.store.getOutOfBandMembers(roomId); - if (rawMembersEvents === null) { - fromServer = true; - const lastEventId = room.getLastEventId(); - const response = await this.members(roomId, "join", "leave", lastEventId); - rawMembersEvents = response.chunk; - console.log(`LL: got ${rawMembersEvents.length} members from server`); - } - const memberEvents = rawMembersEvents.map(this.getEventMapper()); - return {memberEvents, fromServer}; -}; - -/** - * Preloads the member list for the given room id, - * in case lazy loading of memberships is in use. - * @param {string} roomId The room ID - */ -MatrixClient.prototype.loadRoomMembersIfNeeded = async function(roomId) { - const room = this.getRoom(roomId); - if (!room || !room.needsOutOfBandMembers()) { - return; - } - // intercept whether we need to store oob members afterwards - let membersNeedStoring = false; - // Note that we don't await _loadMembers here first. - // setLazyLoadedMembers sets a flag before it awaits the promise passed in - // to avoid a race when calling membersNeedLoading/loadOutOfBandMembers - // in fast succession, before the first promise resolves. - const membersPromise = this._loadMembers(room) - .then(({memberEvents, fromServer}) => { - membersNeedStoring = fromServer; - return memberEvents; - }); - await room.loadOutOfBandMembers(membersPromise); - // if loadOutOfBandMembers throws, this wont be called - // but that's fine as we don't want to store members - // that caused an error. - if (membersNeedStoring) { - const rawMembersEvents = room.currentState.getMembers() - .filter((m) => m.isOutOfBand()) - .map((m) => m.events.member.event); - console.log(`LL: telling backend to store ${rawMembersEvents.length} members`); - await this.store.setOutOfBandMembers(roomId, rawMembersEvents); - } -}; - /** * Retrieve all known rooms. * @return {Room[]} A list of rooms, or an empty list if there is no data store. diff --git a/src/models/room.js b/src/models/room.js index e1d3b9902..d71a9a15c 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -179,6 +179,11 @@ function Room(roomId, myUserId, opts) { this._oobMembersPromise = null; if (this._opts.lazyLoadMembers) { this._oobMembersPromise = new utils.Deferred(); + this._client = client; + if (!this._opts.lazyLoadMembers) { + this._membersPromise = Promise.resolve(); + } else { + this._membersPromise = null; } } @@ -275,41 +280,79 @@ Room.prototype.setSyncedMembership = function(membership) { this._syncedMembership = membership; }; -/** - * 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 - */ -Room.prototype.needsOutOfBandMembers = function() { - return this._opts.lazyLoadMembers && - this.currentState.needsOutOfBandMembers(); +Room.prototype._loadMembersFromServer = async function() { + const queryString = utils.encodeParams({ + membership: "join", + not_membership: "leave", + at: this.getLastEventId(), + }); + const path = utils.encodeUri("/rooms/$roomId/members?" + queryString, + {$roomId: this.roomId}); + const http = this._client._http; + const response = await http.authedRequest(callback, "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}; }; /** - * Loads the out-of-band members from the promise passed in - * @param {Promise} eventsPromise promise that resolves to an array with membership MatrixEvents for the members + * 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.loadOutOfBandMembers = function(eventsPromise) { - if (!this.needsOutOfBandMembers()) { - return Promise.resolve(); +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(); - // store the promise that already updated the room state - // to ensure that happens first - const updatedRoomStatePromise = eventsPromise.then((events) => { - this.currentState.setOutOfBandMembers(events); - }, (err) => { - this.currentState.markOutOfBandMembersFailed(); - throw err; //rethrow so calling code is aware operation failed + const promise = this._loadMembers().then(({memberEvents, fromServer}) => { + this.currentState.setOutOfBandMembers(memberEvents); + if (fromServer) { + const oobMembers = this.currentState.getMembers() + .filter((m) => m.isOutOfBand()) + .map((m) => m.events.member.event); + console.log(`LL: telling store to write ${oobEvents.length}` + + ` members for room ${this.roomId}`); + const store = this._client.store; + return store.setOutOfBandMembers(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) => { + // allow retries on fail + this._membersPromise = null; + this.currentState.markOutOfBandMembersFailed() + throw err; }); - // resolve the Deferred with the pending updated room promise, - // this will signal OOB members are now available to - // dependant code like getEncryptionTargetMembers - if (this._oobMembersPromise) { - this._oobMembersPromise.resolve(updatedRoomStatePromise); - } - return updatedRoomStatePromise; + + this._membersPromise = promise; + return this._membersPromise; }; /** diff --git a/src/sync.js b/src/sync.js index fd893293e..4aa2d2482 100644 --- a/src/sync.js +++ b/src/sync.js @@ -113,7 +113,7 @@ function SyncApi(client, opts) { */ SyncApi.prototype.createRoom = function(roomId) { const client = this.client; - const room = new Room(roomId, client.getUserId(), { + const room = new Room(roomId, client, client.getUserId(), { lazyLoadMembers: this.opts.lazyLoadMembers, pendingEventOrdering: this.opts.pendingEventOrdering, timelineSupport: client.timelineSupport, From 0c6e47a5bcde4299eb0d50d6b53801d048ab2b09 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 7 Aug 2018 13:09:20 +0200 Subject: [PATCH 129/472] await for LL members in getEncryptionTargetUsers --- src/models/room.js | 10 +++------- src/utils.js | 9 --------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index d71a9a15c..f13f8c2b7 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -103,7 +103,7 @@ function synthesizeReceipt(userId, event, receiptType) { * @prop {*} storageToken A token which a data store can use to remember * the state of the room. */ -function Room(roomId, myUserId, opts) { +function Room(roomId, client, myUserId, opts) { opts = opts || {}; opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological"; @@ -176,9 +176,7 @@ function Room(roomId, myUserId, opts) { this._syncedMembership = null; this._summaryHeroes = null; // awaited by getEncryptionTargetMembers while room members are loading - this._oobMembersPromise = null; - if (this._opts.lazyLoadMembers) { - this._oobMembersPromise = new utils.Deferred(); + this._client = client; if (!this._opts.lazyLoadMembers) { this._membersPromise = Promise.resolve(); @@ -626,9 +624,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline, * we should encrypt messages for in this room. */ Room.prototype.getEncryptionTargetMembers = async function() { - if (this._oobMembersPromise) { - await this._oobMembersPromise; - } + await this.loadMembersIfNeeded(); let members = this.getMembersWithMembership("join"); if (this.shouldEncryptForInvitedMembers()) { members = members.concat(this.getMembersWithMembership("invite")); diff --git a/src/utils.js b/src/utils.js index 20993052d..1587c64a3 100644 --- a/src/utils.js +++ b/src/utils.js @@ -672,12 +672,3 @@ module.exports.removeHiddenChars = function(str) { return str.normalize('NFD').replace(removeHiddenCharsRegex, ''); }; const removeHiddenCharsRegex = /[\u200B-\u200D\u0300-\u036f\uFEFF\s]/g; - -module.exports.Deferred = function Deferred() { - let resolve = null; - const promise = new Promise((r) => { - resolve = r; - }); - promise.resolve = resolve; - return promise; -}; From 827db37eeff1279fc67df58585b063bce3957e84 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 7 Aug 2018 13:54:26 +0200 Subject: [PATCH 130/472] fixup --- spec/unit/room.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 274a7d19d..6a01e7b6e 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -469,7 +469,7 @@ describe("Room", function() { describe("compareEventOrdering", function() { beforeEach(function() { - room = new Room(roomId, null, null, null, {timelineSupport: true}); + room = new Room(roomId, null, null, {timelineSupport: true}); }); const events = [ From 9739c3355a61c823a0eb937a901d472e32a97918 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 7 Aug 2018 14:43:19 +0200 Subject: [PATCH 131/472] undo postponing tracking device keys on turning on room encryption --- src/crypto/index.js | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 9fab416f6..8b35bc0a4 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -644,20 +644,19 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic }); this._roomEncryptors[roomId] = alg; + // make sure we are tracking the device lists for all users in this room. + console.log("Enabling encryption in " + roomId + "; " + + "starting to track device lists for all users therein"); + const room = this._clientStore.getRoom(roomId); + if (!room) { + throw new Error(`Unable to enable encryption in unknown room ${roomId}`); + } + const members = await room.getEncryptionTargetMembers(); + members.forEach((m) => { + this._deviceList.startTrackingDeviceList(m.userId); + }); if (!inhibitDeviceQuery) { - // make sure we are tracking the device lists for all users in this room. - console.log("Enabling encryption in " + roomId + "; " + - "starting to track device lists for all users therein"); - const room = this._clientStore.getRoom(roomId); - if (!room) { - throw new Error(`Unable to enable encryption in unknown room ${roomId}`); - } - - const members = await room.getEncryptionTargetMembers(); - members.forEach((m) => { - this._deviceList.startTrackingDeviceList(m.userId); - }); this._deviceList.refreshOutdatedDeviceLists(); } }; From 535d59db4d99a4595786bdec95b705058979fcf7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 7 Aug 2018 14:43:34 +0200 Subject: [PATCH 132/472] fixup --- src/models/room.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index f13f8c2b7..5039f46c2 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -287,7 +287,7 @@ Room.prototype._loadMembersFromServer = async function() { const path = utils.encodeUri("/rooms/$roomId/members?" + queryString, {$roomId: this.roomId}); const http = this._client._http; - const response = await http.authedRequest(callback, "GET", path); + const response = await http.authedRequest(undefined, "GET", path); return response.chunk; }; @@ -331,10 +331,10 @@ Room.prototype.loadMembersIfNeeded = function() { const oobMembers = this.currentState.getMembers() .filter((m) => m.isOutOfBand()) .map((m) => m.events.member.event); - console.log(`LL: telling store to write ${oobEvents.length}` + console.log(`LL: telling store to write ${oobMembers.length}` + ` members for room ${this.roomId}`); const store = this._client.store; - return store.setOutOfBandMembers(roomId, oobMembers) + return store.setOutOfBandMembers(this.roomId, oobMembers) // swallow any IDB error as we don't want to fail // because of this .catch((err) => { From 91f2bf99c007700e2b52486d56e63fddbcccc732 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 7 Aug 2018 14:45:29 +0200 Subject: [PATCH 133/472] fix lint --- src/models/room.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 5039f46c2..4bc285ebd 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -68,6 +68,7 @@ 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 @@ -176,7 +177,7 @@ function Room(roomId, client, myUserId, opts) { this._syncedMembership = null; this._summaryHeroes = null; // awaited by getEncryptionTargetMembers while room members are loading - + this._client = client; if (!this._opts.lazyLoadMembers) { this._membersPromise = Promise.resolve(); @@ -295,7 +296,7 @@ Room.prototype._loadMembersFromServer = async function() { Room.prototype._loadMembers = async function() { // were the members loaded from the server? let fromServer = false; - let rawMembersEvents = + let rawMembersEvents = await this._client.store.getOutOfBandMembers(this.roomId); if (rawMembersEvents === null) { fromServer = true; @@ -345,7 +346,7 @@ Room.prototype.loadMembersIfNeeded = function() { }).catch((err) => { // allow retries on fail this._membersPromise = null; - this.currentState.markOutOfBandMembersFailed() + this.currentState.markOutOfBandMembersFailed(); throw err; }); From 07e87915ba88a1b5a3f3a49a60fe0e00e06a4ba2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 7 Aug 2018 15:24:56 +0200 Subject: [PATCH 134/472] fix and add tests --- spec/unit/room.spec.js | 84 +++++++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 22 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 6a01e7b6e..4e40ed53d 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -387,7 +387,7 @@ describe("Room", function() { let events = null; beforeEach(function() { - room = new Room(roomId, null, {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 = [ @@ -658,7 +658,7 @@ describe("Room", function() { beforeEach(function() { // no mocking - room = new Room(roomId, userA); + room = new Room(roomId, null, userA); }); describe("Room.recalculate => Stripped State Events", function() { @@ -1192,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, userA, { + const room = new Room(roomId, null, userA, { pendingEventOrdering: "detached", }); const eventA = utils.mkMessage({ @@ -1218,7 +1218,7 @@ describe("Room", function() { it("should add pending events to the timeline if " + "pendingEventOrdering == 'chronological'", function() { - room = new Room(roomId, userA, { + room = new Room(roomId, null, userA, { pendingEventOrdering: "chronological", }); const eventA = utils.mkMessage({ @@ -1242,7 +1242,7 @@ describe("Room", function() { describe("updatePendingEvent", function() { it("should remove cancelled events from the pending list", function() { - const room = new Room(roomId, userA, { + const room = new Room(roomId, null, userA, { pendingEventOrdering: "detached", }); const eventA = utils.mkMessage({ @@ -1278,7 +1278,7 @@ describe("Room", function() { it("should remove cancelled events from the timeline", function() { - const room = new Room(roomId, userA); + const room = new Room(roomId, null, userA); const eventA = utils.mkMessage({ room: roomId, user: userA, event: true, }); @@ -1311,53 +1311,93 @@ describe("Room", function() { }); }); - describe("loadOutOfBandMembers", function() { + describe("loadMembersIfNeeded", function() { + function createClientMock(serverResponse, storageResponse = null) { + return { + getEventMapper: function() { + // events should already be MatrixEvents + return function(event) {return event;}; + }, + _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(); + }, + }, + }; + } + const memberEvent = utils.mkMembership({ user: "@user_a:bar", mship: "join", room: roomId, event: true, name: "User A", }); - it("should apply member events", async function() { - const room = new Room(roomId, null); - await room.loadOutOfBandMembers(Promise.resolve([memberEvent])); + 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 apply first call, not first resolved promise", async function() { + 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 room = new Room(roomId, null); + const client = createClientMock([memberEvent2], [memberEvent]); + const room = new Room(roomId, client, null, {lazyLoadMembers: true}); - const promise2 = Promise.resolve([memberEvent2]); - const promise1 = promise2.then(() => [memberEvent]); - - await room.loadOutOfBandMembers(promise1); - await room.loadOutOfBandMembers(promise2); + await room.loadMembersIfNeeded(); const memberA = room.getMember("@user_a:bar"); expect(memberA.name).toEqual("User A"); }); - it("should revert needs loading on error", async function() { - const room = new Room(roomId, null); + 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.loadOutOfBandMembers(Promise.reject(new Error("bugger"))); + await room.loadMembersIfNeeded(); } catch(err) { hasThrown = true; } expect(hasThrown).toEqual(true); - expect(room.needsOutOfBandMembers()).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", async function() { - const room = new Room(roomId, userA); + const room = new Room(roomId, null, userA); room.setSyncedMembership("invite"); expect(room.getMyMembership()).toEqual("invite"); room.addLiveEvents([utils.mkMembership({ From 03c63d9b12f092ab221b61a09b4ee2c4c402638d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 13 Aug 2018 15:11:19 +0200 Subject: [PATCH 135/472] use sync token for /members request, as synapse expects it now --- src/models/room.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 4bc285ebd..10ed00dce 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -227,14 +227,6 @@ Room.prototype.getLiveTimeline = function() { return this.getUnfilteredTimelineSet().getLiveTimeline(); }; -/** - * @return {string} the id of the last event in the live timeline - */ -Room.prototype.getLastEventId = function() { - const liveEvents = this.getLiveTimeline().getEvents(); - return liveEvents.length ? liveEvents[liveEvents.length - 1].getId() : undefined; -}; - /** * @param {string} myUserId the user id for the logged in member * @return {string} the membership type (join | leave | invite) for the logged in user @@ -280,10 +272,11 @@ Room.prototype.setSyncedMembership = function(membership) { }; Room.prototype._loadMembersFromServer = async function() { + const lastSyncToken = this._client.store.getSyncToken(); const queryString = utils.encodeParams({ membership: "join", not_membership: "leave", - at: this.getLastEventId(), + at: lastSyncToken, }); const path = utils.encodeUri("/rooms/$roomId/members?" + queryString, {$roomId: this.roomId}); From 482eab0e2a278f8d6775aec3483c473beaa0ac08 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 13 Aug 2018 15:15:56 +0200 Subject: [PATCH 136/472] fix tests --- spec/unit/room.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 4e40ed53d..eb5ba5a51 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1342,6 +1342,7 @@ describe("Room", function() { this.storedMembers = memberEvents; return Promise.resolve(); }, + getSyncToken: () => "sync_token", }, }; } From 2f4d8c3530a40ff89f325d6c45286b9c790a6b5f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 13 Aug 2018 16:20:12 +0200 Subject: [PATCH 137/472] check with server if it supports member lazy loading --- src/client.js | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 40c491158..b9ad51bcb 100644 --- a/src/client.js +++ b/src/client.js @@ -203,6 +203,8 @@ function MatrixClient(opts) { // The pushprocessor caches useful things, so keep one and re-use it this._pushProcessor = new PushProcessor(this); + + this._serverSupportsLazyLoading = null; } utils.inherits(MatrixClient, EventEmitter); utils.extend(MatrixClient.prototype, MatrixBaseApis.prototype); @@ -3055,7 +3057,14 @@ MatrixClient.prototype.startClient = async function(opts) { opts = Object.assign({}, opts); if (opts.lazyLoadMembers) { - opts.filter = await this.createFilter(LAZY_LOADING_SYNC_FILTER); + const supported = await this.doesServerSupportLazyLoading(); + if (supported) { + opts.filter = await this.createFilter(LAZY_LOADING_SYNC_FILTER); + } else { + console.log("LL: lazy loading requested but not supported " + + "by server, so disabling"); + opts.lazyLoadMembers = false; + } } opts.crypto = this._crypto; @@ -3093,6 +3102,28 @@ MatrixClient.prototype.stopClient = function() { global.clearTimeout(this._checkTurnServersTimeoutID); }; +/* + * Query the server to see if it support members lazy loading + * @return {Promise} true if server supports lazy loading + */ +MatrixClient.prototype.doesServerSupportLazyLoading = async function() { + if (this._serverSupportsLazyLoading === null) { + const response = await this._http.request( + undefined, // callback + "GET", "/_matrix/client/versions", + undefined, // queryParams + undefined, // data + { + prefix: '', + }, + ); + const unstableFeatures = response["unstable_features"]; + this._serverSupportsLazyLoading = + unstableFeatures && unstableFeatures["m.lazy_load_members"]; + } + return this._serverSupportsLazyLoading; +}; + /* * Set a function which is called when /sync returns a 'limited' response. * It is called with a room ID and returns a boolean. It should return 'true' if the SDK From 372a628cab5ebcc7a9e1fa06f9d44482fb6c9cd6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 14 Aug 2018 12:02:38 +0200 Subject: [PATCH 138/472] fix log whitespace --- src/models/room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room.js b/src/models/room.js index 10ed00dce..92983d403 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -294,7 +294,7 @@ Room.prototype._loadMembers = async function() { if (rawMembersEvents === null) { fromServer = true; rawMembersEvents = await this._loadMembersFromServer(); - console.log(`LL: got ${rawMembersEvents.length}` + + console.log(`LL: got ${rawMembersEvents.length} ` + `members from server for room ${this.roomId}`); } const memberEvents = rawMembersEvents.map(this._client.getEventMapper()); From fa16da86b3056e807f0803e4a57a97db3fcf2a7a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 16 Aug 2018 15:17:35 +0100 Subject: [PATCH 139/472] Prepare changelog for v0.10.8-rc.1 --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4bf276ef..9e5ab162d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +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) From eeddfd491987a9b97d95b86fcf8a26954d0bde6b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 16 Aug 2018 15:17:36 +0100 Subject: [PATCH 140/472] v0.10.8-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 15eac9216..64bceadd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.10.7", + "version": "0.10.8-rc.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From e8610a35b48006c10e0106cdea90a6e62c9ced14 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Aug 2018 14:55:12 +0100 Subject: [PATCH 141/472] Support for room upgrades For https://github.com/vector-im/riot-web/issues/7164 --- src/base-apis.js | 12 ++++++++++++ src/models/room.js | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/base-apis.js b/src/base-apis.js index a85ed9bc6..c28235de1 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -446,6 +446,18 @@ function(roomId, includeMembership, excludeMembership, atEventId, callback) { 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 diff --git a/src/models/room.js b/src/models/room.js index 92983d403..25a9ef5ef 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -29,6 +29,9 @@ const EventTimelineSet = require("./event-timeline-set"); import ReEmitter from '../ReEmitter'; +//const LATEST_ROOM_VERSION = '1'; +const LATEST_ROOM_VERSION = 'badger'; + 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 @@ -200,6 +203,27 @@ Room.prototype.getVersion = function() { 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. + */ +Room.prototype.shouldUpgradeToVersion = function() { + // This almost certainly won't be the way this actually works - this + // is essentially a stub method. + if (this.getVersion() === LATEST_ROOM_VERSION) return null; + return LATEST_ROOM_VERSION; +}; + +/** + * 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 * From 8b1fa728770c4a792d907fe82f15f8927edde4b2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Aug 2018 14:59:34 +0100 Subject: [PATCH 142/472] Copyright & debadgering --- src/models/room.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 25a9ef5ef..e95bc6850 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -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. @@ -29,8 +30,7 @@ const EventTimelineSet = require("./event-timeline-set"); import ReEmitter from '../ReEmitter'; -//const LATEST_ROOM_VERSION = '1'; -const LATEST_ROOM_VERSION = 'badger'; +const LATEST_ROOM_VERSION = '1'; function synthesizeReceipt(userId, event, receiptType) { // console.log("synthesizing receipt for "+event.getId()); From 0e3cb1977fea4e0bba67e65ba6a4bb192786a88f Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Aug 2018 15:00:54 +0100 Subject: [PATCH 143/472] lint --- src/base-apis.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/base-apis.js b/src/base-apis.js index c28235de1..9214d8211 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -455,7 +455,9 @@ function(roomId, includeMembership, excludeMembership, atEventId, callback) { */ 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}); + return this._http.authedRequest( + undefined, "POST", path, undefined, {new_version: newVersion}, + ); }; From baca20b22579725ba4869aaca33c1054d388de0e Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 20 Aug 2018 11:14:45 +0100 Subject: [PATCH 144/472] Add support for /rooms/$roomId/event/$eventId --- src/client.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/client.js b/src/client.js index b9ad51bcb..c33d02a7a 100644 --- a/src/client.js +++ b/src/client.js @@ -2071,6 +2071,28 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) { return promise; }; +/** + * Get an event in a room by its event id. + * + * This blindly calls the /rooms/$roomId/event/$eventId endpoint. + * + * @param {string} roomId The ID of the room to look in + * @param {string} eventId The ID of the event to look for + * + * @return {Promise} Resolves to an object containing the event. + */ +MatrixClient.prototype.fetchRoomEvent = function(roomId, eventId) { + const path = utils.encodeUri( + "/rooms/$roomId/event/$eventId", { + $roomId: roomId, + $eventId: eventId, + }, + ); + return this._http.authedRequest( + callback, "GET", path, + ); +} + /** * Makes a request to /messages with the appropriate lazy loading filter set. * XXX: if we do get rid of scrollback (as it's not used at the moment), From 79ca68300cb5f2276cc3d351aaa9cd6db8bc015d Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 20 Aug 2018 12:47:26 +0100 Subject: [PATCH 145/472] Add callback --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index c33d02a7a..8f8884b2c 100644 --- a/src/client.js +++ b/src/client.js @@ -2081,7 +2081,7 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) { * * @return {Promise} Resolves to an object containing the event. */ -MatrixClient.prototype.fetchRoomEvent = function(roomId, eventId) { +MatrixClient.prototype.fetchRoomEvent = function(roomId, eventId, callback) { const path = utils.encodeUri( "/rooms/$roomId/event/$eventId", { $roomId: roomId, From 88c5c39fcb9d2b82858ac65f5edf176564b6e981 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 20 Aug 2018 13:44:29 +0100 Subject: [PATCH 146/472] Prepare changelog for v0.10.8 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e5ab162d..a97cb1ed8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +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) From 4d310cd4618db4e98a8e6b5eb812480102ee4dee Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 20 Aug 2018 13:44:30 +0100 Subject: [PATCH 147/472] v0.10.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 64bceadd0..13344acaa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.10.8-rc.1", + "version": "0.10.8", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From df7dc04a1d66718c9bcbd5745ff1ca21613c4b95 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 20 Aug 2018 14:16:47 +0100 Subject: [PATCH 148/472] Happy linter, happy developer --- src/client.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 8f8884b2c..6ef6e9dfd 100644 --- a/src/client.js +++ b/src/client.js @@ -2078,6 +2078,7 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) { * * @param {string} roomId The ID of the room to look in * @param {string} eventId The ID of the event to look for + * @param {module:client.callback} callback Optional. * * @return {Promise} Resolves to an object containing the event. */ @@ -2091,7 +2092,7 @@ MatrixClient.prototype.fetchRoomEvent = function(roomId, eventId, callback) { return this._http.authedRequest( callback, "GET", path, ); -} +}; /** * Makes a request to /messages with the appropriate lazy loading filter set. From 3d24c8768f748671947858f2d5769b3489923d18 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 21 Aug 2018 11:07:37 +0100 Subject: [PATCH 149/472] Drop fetchRoomEvent from client.js --- src/client.js | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/client.js b/src/client.js index 6ef6e9dfd..b9ad51bcb 100644 --- a/src/client.js +++ b/src/client.js @@ -2071,29 +2071,6 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) { return promise; }; -/** - * Get an event in a room by its event id. - * - * This blindly calls the /rooms/$roomId/event/$eventId endpoint. - * - * @param {string} roomId The ID of the room to look in - * @param {string} eventId The ID of the event to look for - * @param {module:client.callback} callback Optional. - * - * @return {Promise} Resolves to an object containing the event. - */ -MatrixClient.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, - ); -}; - /** * Makes a request to /messages with the appropriate lazy loading filter set. * XXX: if we do get rid of scrollback (as it's not used at the moment), From 5b830f0b6a78e327a9fdca24ab0fe5acd68e3e51 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 21 Aug 2018 11:07:48 +0100 Subject: [PATCH 150/472] Add fetchRoomEvent to base-apis.js --- src/base-apis.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/base-apis.js b/src/base-apis.js index a85ed9bc6..73527ec44 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -417,6 +417,25 @@ 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 From 962ec7bb532812d92cb4d5140f34cef4274718c4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Aug 2018 11:58:44 +0100 Subject: [PATCH 151/472] Add getVisibleRooms() To hide rooms that have been replaced For https://github.com/vector-im/riot-web/issues/7164 --- src/client.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/client.js b/src/client.js index b9ad51bcb..595246798 100644 --- a/src/client.js +++ b/src/client.js @@ -779,6 +779,37 @@ MatrixClient.prototype.getRooms = function() { return this.store.getRooms(); }; +/** + * Retrieve all rooms that should be displayed to the user + * This is essentially getRooms() with some rooms filtered out, eg. old versions + * of rooms that have been replaced or (in future) other rooms that have been + * markewd at the protocol level as not to be displayed to the user. + * @return {Room[]} A list of rooms, or an empty list if there is no data store. + */ +MatrixClient.prototype.getVisibleRooms = function() { + const allRooms = this.store.getRooms(); + + const replacedRooms = new Set(); + for (const r of allRooms) { + const createEvent = r.currentState.getStateEvents('m.room.create', ''); + // invites are included in this list and we don't know their create events yet + if (createEvent) { + const predecessor = createEvent.getContent()['predecessor']; + if (predecessor && predecessor['room_id']) { + replacedRooms.add(predecessor['room_id']); + } + } + } + + return allRooms.filter((r) => { + const tombstone = r.currentState.getStateEvents('m.room.tombstone', ''); + if (tombstone && replacedRooms.has(r.roomId)) { + return false; + } + return true; + }); +}; + /** * Retrieve a user. * @param {string} userId The user ID to retrieve. From 71444b638bd3786df7df556fd2e1b0f4be7ffeee Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Aug 2018 14:45:36 +0200 Subject: [PATCH 152/472] 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. --- src/crypto/RoomList.js | 3 +++ src/sync.js | 9 +++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/crypto/RoomList.js b/src/crypto/RoomList.js index 5bb437fb7..eb5b469c6 100644 --- a/src/crypto/RoomList.js +++ b/src/crypto/RoomList.js @@ -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 for calling + // this twice for consecutive m.room.encryption events this._roomEncryption[roomId] = roomInfo; await this._cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => { diff --git a/src/sync.js b/src/sync.js index 4aa2d2482..d8dffe6b3 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1085,15 +1085,16 @@ SyncApi.prototype._processSyncResponse = async function( self._processEventsForNotifs(room, timelineEvents); - async function processRoomEvent(e) { + function processRoomEvent(e) { client.emit("event", e); if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) { - await self.opts.crypto.onCryptoEvent(e); + self.opts.crypto.onCryptoEvent(e); } } - await Promise.mapSeries(stateEvents, processRoomEvent); - await Promise.mapSeries(timelineEvents, processRoomEvent); + stateEvents.forEach(processRoomEvent); + timelineEvents.forEach(processRoomEvent); + ephemeralEvents.forEach(function(e) { client.emit("event", e); }); From 4c3046f917e9ee6606070be3c7a5f36baefc2db8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 22 Aug 2018 16:34:06 +0100 Subject: [PATCH 153/472] 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 --- src/models/room.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/models/room.js b/src/models/room.js index e95bc6850..cf8390ab7 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -625,6 +625,17 @@ 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(); +}; + /** * Get a list of members with given membership state. * @param {string} membership The membership state. From 267d660527c272eef40a116b6ffe473819803353 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Aug 2018 17:42:30 +0200 Subject: [PATCH 154/472] add invite count to room as well --- src/models/room.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/models/room.js b/src/models/room.js index cf8390ab7..94115ef45 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -636,6 +636,14 @@ 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(); +}; + /** * Get a list of members with given membership state. * @param {string} membership The membership state. From aea9eaa3078d44e1cf3aea8f2bb3a9e02f0f3ff1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Aug 2018 17:40:13 +0200 Subject: [PATCH 155/472] Only start tracking devices in an e2e room when needed This way we can put off loading the members --- src/crypto/index.js | 93 ++++++++++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 39 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 8b35bc0a4..be2cc68fe 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -106,6 +106,10 @@ function Crypto(baseApis, sessionStore, userId, deviceId, this._receivedRoomKeyRequestCancellations = []; // true if we are currently processing received room key requests this._processingRoomKeyRequests = false; + // track if an initial tracking of all the room members + // has happened for a given room. This is delayed + // to avoid loading room members as long as possible. + this._roomDeviceTrackingState = {}; } utils.inherits(Crypto, EventEmitter); @@ -612,11 +616,8 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) { * @param {string} roomId The room ID to enable encryption in. * * @param {object} config The encryption config for the room. - * - * @param {boolean=} inhibitDeviceQuery true to suppress device list query for - * users in the room (for now) */ -Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDeviceQuery) { +Crypto.prototype.setRoomEncryption = async function(roomId, config) { // if we already have encryption in this room, we should ignore this event // (for now at least. maybe we should alert the user somehow?) const existingConfig = this._roomList.getRoomEncryption(roomId); @@ -644,22 +645,27 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic }); this._roomEncryptors[roomId] = alg; - // make sure we are tracking the device lists for all users in this room. - console.log("Enabling encryption in " + roomId + "; " + - "starting to track device lists for all users therein"); + console.log("Enabling encryption in " + roomId); +}; + + +/** + * Make sure we are tracking the device lists for all users in this room. + * + * @param {string} roomId The room ID to start tracking devices in. + */ +Crypto.prototype._trackRoomDevices = async function(roomId) { const room = this._clientStore.getRoom(roomId); if (!room) { - throw new Error(`Unable to enable encryption in unknown room ${roomId}`); + throw new Error(`Unable to start tracking devices in unknown room ${roomId}`); } - + console.log(`Starting to track devices for room ${roomId} ...`); const members = await room.getEncryptionTargetMembers(); members.forEach((m) => { this._deviceList.startTrackingDeviceList(m.userId); }); - if (!inhibitDeviceQuery) { - this._deviceList.refreshOutdatedDeviceLists(); - } -}; + return this._deviceList.refreshOutdatedDeviceLists(); +} /** @@ -763,7 +769,7 @@ Crypto.prototype.importRoomKeys = function(keys) { * @return {module:client.Promise?} Promise which resolves when the event has been * encrypted, or null if nothing was needed */ -Crypto.prototype.encryptEvent = function(event, room) { +Crypto.prototype.encryptEvent = async function(event, room) { if (!room) { throw new Error("Cannot send encrypted messages in unknown rooms"); } @@ -781,6 +787,12 @@ Crypto.prototype.encryptEvent = function(event, room) { ); } + if (!this._roomDeviceTrackingState[roomId]) { + this._roomDeviceTrackingState[roomId] = this._trackRoomDevices(roomId); + } + // wait for all the room devices to be loaded + await this._roomDeviceTrackingState[roomId]; + let content = event.getContent(); // If event has an m.relates_to then we need // to put this on the wrapping event instead @@ -791,20 +803,19 @@ Crypto.prototype.encryptEvent = function(event, room) { delete content['m.relates_to']; } - return alg.encryptMessage( - room, event.getType(), content, - ).then((encryptedContent) => { - if (mRelatesTo) { - encryptedContent['m.relates_to'] = mRelatesTo; - } + const encryptedContent = await alg.encryptMessage( + room, event.getType(), content) - event.makeEncrypted( - "m.room.encrypted", - encryptedContent, - this._olmDevice.deviceCurve25519Key, - this._olmDevice.deviceEd25519Key, - ); - }); + if (mRelatesTo) { + encryptedContent['m.relates_to'] = mRelatesTo; + } + + event.makeEncrypted( + "m.room.encrypted", + encryptedContent, + this._olmDevice.deviceCurve25519Key, + this._olmDevice.deviceEd25519Key, + ); }; /** @@ -897,9 +908,7 @@ Crypto.prototype.onCryptoEvent = async function(event) { const content = event.getContent(); try { - // inhibit the device list refresh for now - it will happen once we've - // finished processing the sync, in onSyncCompleted. - await this.setRoomEncryption(roomId, content, true); + await this.setRoomEncryption(roomId, content); } catch (e) { console.error("Error configuring encryption in room " + roomId + ":", e); @@ -919,6 +928,7 @@ Crypto.prototype.onSyncWillProcess = async function(syncData) { // at which point we'll start tracking all the users of that room. console.log("Initial sync performed - resetting device tracking state"); this._deviceList.stopTrackingAllDeviceLists(); + this._roomDeviceTrackingState = {}; } }; @@ -1074,15 +1084,20 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) { // not encrypting in this room return; } - - if (member.membership == 'join') { - console.log('Join event for ' + member.userId + ' in ' + roomId); - // make sure we are tracking the deviceList for this user - this._deviceList.startTrackingDeviceList(member.userId); - } else if (member.membership == 'invite' && - this._clientStore.getRoom(roomId).shouldEncryptForInvitedMembers()) { - console.log('Invite event for ' + member.userId + ' in ' + roomId); - this._deviceList.startTrackingDeviceList(member.userId); + // only mark users in this room as tracked if we already started tracking in this room + // this way we don't start device queries after sync on behalf of this room which we won't use + // the result of anyway, as we'll need to do a query again once all the members are fetched + // by calling _trackRoomDevices + if (this._roomDeviceTrackingState[roomId]) { + if (member.membership == 'join') { + console.log('Join event for ' + member.userId + ' in ' + roomId); + // make sure we are tracking the deviceList for this user + this._deviceList.startTrackingDeviceList(member.userId); + } else if (member.membership == 'invite' && + this._clientStore.getRoom(roomId).shouldEncryptForInvitedMembers()) { + console.log('Invite event for ' + member.userId + ' in ' + roomId); + this._deviceList.startTrackingDeviceList(member.userId); + } } alg.onRoomMembership(event, member, oldMembership); From 344e3e18ab98f003bdec54eff88f5d60f9e6b4fa Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Aug 2018 19:13:18 +0200 Subject: [PATCH 156/472] start tracking room devices in background after finishing loading members --- src/crypto/index.js | 56 ++++++++++++++++++++++++++++++--------------- src/models/room.js | 5 ++++ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index be2cc68fe..dcbf2bc3c 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -654,19 +654,31 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config) { * * @param {string} roomId The room ID to start tracking devices in. */ -Crypto.prototype._trackRoomDevices = async function(roomId) { - const room = this._clientStore.getRoom(roomId); - if (!room) { - throw new Error(`Unable to start tracking devices in unknown room ${roomId}`); - } - console.log(`Starting to track devices for room ${roomId} ...`); - const members = await room.getEncryptionTargetMembers(); - members.forEach((m) => { - this._deviceList.startTrackingDeviceList(m.userId); - }); - return this._deviceList.refreshOutdatedDeviceLists(); -} +Crypto.prototype.trackRoomDevices = function(roomId) { + const trackAndRefresh = async () => { + // not an encrypted room + if (!this._roomEncryptors[roomId]) { + return; + } + const room = this._clientStore.getRoom(roomId); + if (!room) { + throw new Error(`Unable to start tracking devices in unknown room ${roomId}`); + } + console.log(`Starting to track devices for room ${roomId} ...`); + const members = await room.getEncryptionTargetMembers(); + members.forEach((m) => { + this._deviceList.startTrackingDeviceList(m.userId); + }); + return refreshPromise = this._deviceList.refreshOutdatedDeviceLists(); + }; + let promise = this._roomDeviceTrackingState[roomId]; + if (!promise) { + promise = trackAndRefresh(); + this._roomDeviceTrackingState[roomId] = promise; + } + return promise; +} /** * @typedef {Object} module:crypto~OlmSessionResult @@ -788,7 +800,7 @@ Crypto.prototype.encryptEvent = async function(event, room) { } if (!this._roomDeviceTrackingState[roomId]) { - this._roomDeviceTrackingState[roomId] = this._trackRoomDevices(roomId); + this.trackRoomDevices(roomId); } // wait for all the room devices to be loaded await this._roomDeviceTrackingState[roomId]; @@ -974,11 +986,12 @@ Crypto.prototype._evalDeviceListChanges = async function(deviceLists) { }); } - if (deviceLists.left && Array.isArray(deviceLists.left)) { + if (deviceLists.left && Array.isArray(deviceLists.left) && + deviceLists.left.length) { // Check we really don't share any rooms with these users // any more: the server isn't required to give us the // exact correct set. - const e2eUserIds = new Set(await this._getE2eUsers()); + const e2eUserIds = new Set(await this._getTrackedE2eUsers()); deviceLists.left.forEach((u) => { if (!e2eUserIds.has(u)) { @@ -990,12 +1003,13 @@ Crypto.prototype._evalDeviceListChanges = async function(deviceLists) { /** * Get a list of all the IDs of users we share an e2e room with + * for which we are tracking devices already * * @returns {string[]} List of user IDs */ -Crypto.prototype._getE2eUsers = async function() { +Crypto.prototype._getTrackedE2eUsers = async function() { const e2eUserIds = []; - for (const room of this._getE2eRooms()) { + for (const room of this._getTrackedE2eRooms()) { const members = await room.getEncryptionTargetMembers(); for (const member of members) { e2eUserIds.push(member.userId); @@ -1005,17 +1019,21 @@ Crypto.prototype._getE2eUsers = async function() { }; /** - * Get a list of the e2e-enabled rooms we are members of + * Get a list of the e2e-enabled rooms we are members of, + * and for which we are already tracking the devices * * @returns {module:models.Room[]} */ -Crypto.prototype._getE2eRooms = function() { +Crypto.prototype._getTrackedE2eRooms = function() { return this._clientStore.getRooms().filter((room) => { // check for rooms with encryption enabled const alg = this._roomEncryptors[room.roomId]; if (!alg) { return false; } + if (!this._roomDeviceTrackingState[room.roomId]) { + return false; + } // ignore any rooms which we have left const myMembership = room.getMyMembership(); diff --git a/src/models/room.js b/src/models/room.js index e95bc6850..24c542598 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -368,6 +368,11 @@ Room.prototype.loadMembersIfNeeded = function() { }); this._membersPromise = promise; + // 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 this._membersPromise; }; From fa702efe8fa92e7de96908a9ef547912022be14b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Aug 2018 19:22:50 +0200 Subject: [PATCH 157/472] fix typo --- src/crypto/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index dcbf2bc3c..738375ca5 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -669,7 +669,7 @@ Crypto.prototype.trackRoomDevices = function(roomId) { members.forEach((m) => { this._deviceList.startTrackingDeviceList(m.userId); }); - return refreshPromise = this._deviceList.refreshOutdatedDeviceLists(); + return this._deviceList.refreshOutdatedDeviceLists(); }; let promise = this._roomDeviceTrackingState[roomId]; From 806b40727d9b8ca78954f4894bf31e41c1e81d33 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Aug 2018 23:00:27 +0200 Subject: [PATCH 158/472] fix lint --- src/crypto/index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 738375ca5..c512b514e 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -653,6 +653,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config) { * Make sure we are tracking the device lists for all users in this room. * * @param {string} roomId The room ID to start tracking devices in. + * @returns {Promise} when all devices for the room have been fetched and marked to track */ Crypto.prototype.trackRoomDevices = function(roomId) { const trackAndRefresh = async () => { @@ -678,7 +679,7 @@ Crypto.prototype.trackRoomDevices = function(roomId) { this._roomDeviceTrackingState[roomId] = promise; } return promise; -} +}; /** * @typedef {Object} module:crypto~OlmSessionResult @@ -770,7 +771,7 @@ Crypto.prototype.importRoomKeys = function(keys) { }, ); }; - +/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307 /** * Encrypt an event according to the configuration of the room. * @@ -781,6 +782,7 @@ Crypto.prototype.importRoomKeys = function(keys) { * @return {module:client.Promise?} Promise which resolves when the event has been * encrypted, or null if nothing was needed */ +/* eslint-enable valid-jsdoc */ Crypto.prototype.encryptEvent = async function(event, room) { if (!room) { throw new Error("Cannot send encrypted messages in unknown rooms"); @@ -816,7 +818,7 @@ Crypto.prototype.encryptEvent = async function(event, room) { } const encryptedContent = await alg.encryptMessage( - room, event.getType(), content) + room, event.getType(), content); if (mRelatesTo) { encryptedContent['m.relates_to'] = mRelatesTo; From e3d108454cdca0fff98473b425d8b3025e41aa78 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 Aug 2018 23:25:37 +0200 Subject: [PATCH 159/472] fix test --- spec/unit/room.spec.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index eb5ba5a51..b98559ecf 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1318,6 +1318,9 @@ describe("Room", function() { // events should already be MatrixEvents return function(event) {return event;}; }, + isRoomEncrypted: function() { + return false; + }, _http: { serverResponse, authedRequest: function() { From 1faf4775373608ab67c5613a9ff58bda6179c93b Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 22 Aug 2018 23:58:59 -0400 Subject: [PATCH 160/472] fix formatting and fix authedRequest usage --- spec/unit/crypto/backup.spec.js | 63 +++++++++++++++++++++------------ src/client.js | 31 ++++++++-------- 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index b6ec6dce5..a4956647f 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -45,8 +45,10 @@ describe("MegolmBackup", function() { 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/Og0mOBr066SpjqqbTmoK"); + mockCrypto.backupKey = new global.Olm.PkEncryption(); + mockCrypto.backupKey.set_recipient_key( + "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK" + ); mockStorage = new MockStorageApi(); sessionStore = new WebStorageSessionStore(mockStorage); @@ -184,35 +186,50 @@ describe("MegolmBackup", function() { algorithm: 'm.megolm.v1.aes-sha2', sender_key: 'SENDER_CURVE25519', session_id: 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', - ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/NCiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBlmkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs' + ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N' + + 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl' + + 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs' }, event_id: '$event1', origin_server_ts: 1507753886000, }); client._http.authedRequest = function () { return Promise.resolve({ - data: { - first_message_index: 0, - forwarded_count: 0, - is_verified: false, - session_data: { - ciphertext: '2z2M7CZ+azAiTHN1oFzZ3smAFFt+LEOYY6h3QO3XXGdw6YpNn/gpHDO6I/rgj1zNd4FoTmzcQgvKdU8kN20u5BWRHxaHTZSlne5RxE6vUdREsBgZePglBNyG0AogR/PVdcrv/v18Y6rLM5O9SELmwbV63uV9Kuu/misMxoqbuqEdG7uujyaEKtjlQsJ5MGPQOySyw7XrnesSwF6XWRMxcPGRV0xZr3s9PI350Wve3EncjRgJ9IGFru1bcptMqfXgPZkOyGvrphHoFfoK7nY3xMEHUiaTRfRIjq8HNV4o8QY1qmWGnxNBQgOlL8MZlykjg3ULmQ3DtFfQPj/YYGS3jzxvC+EBjaafmsg+52CTeK3Rswu72PX450BnSZ1i3If4xWAUKvjTpeUg5aDLqttOv1pITolTJDw5W/SD+b5rjEKg1CFCHGEGE9wwV3NfQHVCQL+dfpd7Or0poy4dqKMAi3g0o3Tg7edIF8d5rREmxaALPyiie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg', - mac: '5lxYBHQU80M', - ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14', - } - }, - headers: {}, - code: 200 + 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 decryption = new Olm.PkDecryption(); - decryption.unpickle("secret_key", "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZDQWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA"); - return client.restoreKeyBackups(decryption, ROOM_ID, 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc') - .then(() => { - return megolmDecryption.decryptEvent(event); - }).then((res) => { - expect(res.clearEvent.content).toEqual('testytest'); - }); + const decryption = new global.Olm.PkDecryption(); + decryption.unpickle( + "secret_key", + "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" + + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA" + ); + return client.restoreKeyBackups( + decryption, + ROOM_ID, + 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc' + ).then(() => { + return megolmDecryption.decryptEvent(event); + }).then((res) => { + expect(res.clearEvent.content).toEqual('testytest'); + }); }); }); }); diff --git a/src/client.js b/src/client.js index 7aa40d09d..329a58498 100644 --- a/src/client.js +++ b/src/client.js @@ -753,22 +753,21 @@ MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, roomId, sessi const path = this._makeKeyBackupPath(roomId, sessionId, version); return this._http.authedRequest( undefined, "GET", path.path, path.queryData, - ).then((response) => { - if (response.code === 200) { - const keys = []; - // FIXME: for each room, session, if response has multiple - // decrypt response.data.session_data - const data = response.data; - const key = JSON.parse(decryptionKey.decrypt(data.session_data.ephemeral, data.session_data.mac, data.session_data.ciphertext)); - // set room_id and session_id - key.room_id = roomId; - key.session_id = sessionId; - keys.push(key); - return this.importRoomKeys(keys); - } else { - callback("aargh!"); - return Promise.reject("aaargh!"); - } + ).then((res) => { + const keys = []; + // FIXME: for each room, session, if response has multiple + // decrypt response.data.session_data + const session_data = res.session_data; + const key = JSON.parse(decryptionKey.decrypt( + session_data.ephemeral, + session_data.mac, + session_data.ciphertext + )); + // set room_id and session_id + key.room_id = roomId; + key.session_id = sessionId; + keys.push(key); + return this.importRoomKeys(keys); }).then(() => { if (callback) { callback(); From fb8efe368a2538d294af9aee3135bc6e9789a61c Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 23 Aug 2018 00:03:36 -0400 Subject: [PATCH 161/472] initial draft of API for working with backup versions --- src/client.js | 68 ++++++++++++++++++++++++++++++++++++++++++++ src/crypto/olmlib.js | 5 ++++ 2 files changed, 73 insertions(+) diff --git a/src/client.js b/src/client.js index 329a58498..e9f651d49 100644 --- a/src/client.js +++ b/src/client.js @@ -703,6 +703,74 @@ MatrixClient.prototype.importRoomKeys = function(keys) { return this._crypto.importRoomKeys(keys); }; +/** + * Get information about the current key backup. + */ +MatrixClient.prototype.getKeyBackupVersion = function(callback) { + return this._http.authedRequest( + undefined, "GET", "/room_keys/version", + ).then((res) => { + if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) { + const err = "Unknown backup algorithm: " + res.algorithm; + callback(err); + return Promise.reject(err); + } else if (!(typeof res.auth_data === "object") + || !res.auth_data.public_key) { + const err = "Invalid backup data returned"; + callback(err); + return Promise.reject(err); + } else { + if (callback) { + callback(null, res); + } + return res; + } + }); +} + +/** + * Enable backing up of keys, using data previously returned from + * getKeyBackupVersion. + */ +MatrixClient.prototype.enableKeyBackup = function(info) { + this._crypto.backupKey = new global.Olm.PkEncryption(); + this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); +} + +/** + * Disable backing up of keys. + */ +MatrixClient.prototype.disableKeyBackup = function() { + this._crypto.backupKey = undefined; +} + +/** + * Create a new key backup version and enable it. + */ +MatrixClient.prototype.createKeyBackupVersion = function(callback) { + const decryption = new global.Olm.PkDecryption(); + const public_key = decryption.generate_key(); + const encryption = new global.Olm.PkEncryption(); + encryption.set_recipient_key(public_key); + const data = { + algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, + auth_data: { + public_key: public_key, + } + }; + this._crypto._signObject(data.auth_data); + return this._http.authedRequest( + undefined, "POST", "/room_keys/version", undefined, data, + ).then((res) => { + this._crypto.backupKey = encryption; + // FIXME: pickle isn't the right thing to use, but we don't have + // anything else yet + const recovery_key = decryption.pickle(""); + callback(null, recovery_key); + return recovery_key; + }); +} + MatrixClient.prototype._makeKeyBackupPath = function(roomId, sessionId, version) { let path; if (sessionId !== undefined) { diff --git a/src/crypto/olmlib.js b/src/crypto/olmlib.js index 56799c513..f03714f16 100644 --- a/src/crypto/olmlib.js +++ b/src/crypto/olmlib.js @@ -35,6 +35,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"; + /** * Encrypt an event payload for an Olm device From 75107f99b280899619b3de28f6ec1b5c3cdaaec9 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 23 Aug 2018 00:26:21 -0400 Subject: [PATCH 162/472] pass in key rather than decryption object to restoreKeyBackups --- spec/unit/crypto/backup.spec.js | 9 ++------- src/client.js | 8 ++++++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index a4956647f..d13b5b608 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -215,14 +215,9 @@ describe("MegolmBackup", function() { } }); }; - const decryption = new global.Olm.PkDecryption(); - decryption.unpickle( - "secret_key", - "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" - + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA" - ); return client.restoreKeyBackups( - decryption, + "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" + + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", ROOM_ID, 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc' ).then(() => { diff --git a/src/client.js b/src/client.js index e9f651d49..56d546eb1 100644 --- a/src/client.js +++ b/src/client.js @@ -765,7 +765,7 @@ MatrixClient.prototype.createKeyBackupVersion = function(callback) { this._crypto.backupKey = encryption; // FIXME: pickle isn't the right thing to use, but we don't have // anything else yet - const recovery_key = decryption.pickle(""); + const recovery_key = decryption.pickle("secret_key"); callback(null, recovery_key); return recovery_key; }); @@ -818,6 +818,10 @@ MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, roomId, sessi throw new Error("End-to-end encryption disabled"); } + // FIXME: see the FIXME in createKeyBackupVersion + const decryption = new global.Olm.PkDecryption(); + decryption.unpickle("secret_key", decryptionKey); + const path = this._makeKeyBackupPath(roomId, sessionId, version); return this._http.authedRequest( undefined, "GET", path.path, path.queryData, @@ -826,7 +830,7 @@ MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, roomId, sessi // FIXME: for each room, session, if response has multiple // decrypt response.data.session_data const session_data = res.session_data; - const key = JSON.parse(decryptionKey.decrypt( + const key = JSON.parse(decryption.decrypt( session_data.ephemeral, session_data.mac, session_data.ciphertext From e5ec4799231b6618533c9e92d2f326920981bc42 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 23 Aug 2018 00:27:30 -0400 Subject: [PATCH 163/472] check that crypto is enabled --- src/client.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/client.js b/src/client.js index 56d546eb1..926b09355 100644 --- a/src/client.js +++ b/src/client.js @@ -707,6 +707,10 @@ MatrixClient.prototype.importRoomKeys = function(keys) { * Get information about the current key backup. */ MatrixClient.prototype.getKeyBackupVersion = function(callback) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + return this._http.authedRequest( undefined, "GET", "/room_keys/version", ).then((res) => { @@ -733,6 +737,10 @@ MatrixClient.prototype.getKeyBackupVersion = function(callback) { * getKeyBackupVersion. */ MatrixClient.prototype.enableKeyBackup = function(info) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + this._crypto.backupKey = new global.Olm.PkEncryption(); this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); } @@ -741,6 +749,10 @@ MatrixClient.prototype.enableKeyBackup = function(info) { * Disable backing up of keys. */ MatrixClient.prototype.disableKeyBackup = function() { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + this._crypto.backupKey = undefined; } @@ -748,6 +760,10 @@ MatrixClient.prototype.disableKeyBackup = function() { * Create a new key backup version and enable it. */ MatrixClient.prototype.createKeyBackupVersion = function(callback) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + const decryption = new global.Olm.PkDecryption(); const public_key = decryption.generate_key(); const encryption = new global.Olm.PkEncryption(); From 73e294b1bd168e6e465406667987feba458618ef Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 23 Aug 2018 00:29:29 -0400 Subject: [PATCH 164/472] add copyright header to backup.spec --- spec/unit/crypto/backup.spec.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index d13b5b608..f832f0511 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -1,3 +1,18 @@ +/* +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. +*/ try { global.Olm = require('olm'); } catch (e) { From 7c66f91429d9440d4fe6b9305fdcbe441638d111 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 23 Aug 2018 09:34:23 +0100 Subject: [PATCH 165/472] Typo --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 595246798..54f9647f2 100644 --- a/src/client.js +++ b/src/client.js @@ -783,7 +783,7 @@ MatrixClient.prototype.getRooms = function() { * Retrieve all rooms that should be displayed to the user * This is essentially getRooms() with some rooms filtered out, eg. old versions * of rooms that have been replaced or (in future) other rooms that have been - * markewd at the protocol level as not to be displayed to the user. + * marked at the protocol level as not to be displayed to the user. * @return {Room[]} A list of rooms, or an empty list if there is no data store. */ MatrixClient.prototype.getVisibleRooms = function() { From 5cf2ebea4fc2728e5492c108b04120daa81bc610 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 23 Aug 2018 14:05:57 +0200 Subject: [PATCH 166/472] Revert "Lazy loading: don't block on setting up room crypto" --- spec/unit/room.spec.js | 3 - src/crypto/RoomList.js | 3 - src/crypto/index.js | 139 +++++++++++++++-------------------------- src/models/room.js | 5 -- src/sync.js | 9 ++- 5 files changed, 56 insertions(+), 103 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index b98559ecf..eb5ba5a51 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1318,9 +1318,6 @@ describe("Room", function() { // events should already be MatrixEvents return function(event) {return event;}; }, - isRoomEncrypted: function() { - return false; - }, _http: { serverResponse, authedRequest: function() { diff --git a/src/crypto/RoomList.js b/src/crypto/RoomList.js index eb5b469c6..5bb437fb7 100644 --- a/src/crypto/RoomList.js +++ b/src/crypto/RoomList.js @@ -71,9 +71,6 @@ export default class RoomList { } async setRoomEncryption(roomId, roomInfo) { - // important that this happens before calling into the store - // as it prevents the Crypto::setRoomEncryption for calling - // this twice for consecutive m.room.encryption events this._roomEncryption[roomId] = roomInfo; await this._cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => { diff --git a/src/crypto/index.js b/src/crypto/index.js index c512b514e..8b35bc0a4 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -106,10 +106,6 @@ function Crypto(baseApis, sessionStore, userId, deviceId, this._receivedRoomKeyRequestCancellations = []; // true if we are currently processing received room key requests this._processingRoomKeyRequests = false; - // track if an initial tracking of all the room members - // has happened for a given room. This is delayed - // to avoid loading room members as long as possible. - this._roomDeviceTrackingState = {}; } utils.inherits(Crypto, EventEmitter); @@ -616,8 +612,11 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) { * @param {string} roomId The room ID to enable encryption in. * * @param {object} config The encryption config for the room. + * + * @param {boolean=} inhibitDeviceQuery true to suppress device list query for + * users in the room (for now) */ -Crypto.prototype.setRoomEncryption = async function(roomId, config) { +Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDeviceQuery) { // if we already have encryption in this room, we should ignore this event // (for now at least. maybe we should alert the user somehow?) const existingConfig = this._roomList.getRoomEncryption(roomId); @@ -645,42 +644,24 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config) { }); this._roomEncryptors[roomId] = alg; - console.log("Enabling encryption in " + roomId); -}; - - -/** - * Make sure we are tracking the device lists for all users in this room. - * - * @param {string} roomId The room ID to start tracking devices in. - * @returns {Promise} when all devices for the room have been fetched and marked to track - */ -Crypto.prototype.trackRoomDevices = function(roomId) { - const trackAndRefresh = async () => { - // not an encrypted room - if (!this._roomEncryptors[roomId]) { - return; - } - const room = this._clientStore.getRoom(roomId); - if (!room) { - throw new Error(`Unable to start tracking devices in unknown room ${roomId}`); - } - console.log(`Starting to track devices for room ${roomId} ...`); - const members = await room.getEncryptionTargetMembers(); - members.forEach((m) => { - this._deviceList.startTrackingDeviceList(m.userId); - }); - return this._deviceList.refreshOutdatedDeviceLists(); - }; - - let promise = this._roomDeviceTrackingState[roomId]; - if (!promise) { - promise = trackAndRefresh(); - this._roomDeviceTrackingState[roomId] = promise; + // make sure we are tracking the device lists for all users in this room. + console.log("Enabling encryption in " + roomId + "; " + + "starting to track device lists for all users therein"); + const room = this._clientStore.getRoom(roomId); + if (!room) { + throw new Error(`Unable to enable encryption in unknown room ${roomId}`); + } + + const members = await room.getEncryptionTargetMembers(); + members.forEach((m) => { + this._deviceList.startTrackingDeviceList(m.userId); + }); + if (!inhibitDeviceQuery) { + this._deviceList.refreshOutdatedDeviceLists(); } - return promise; }; + /** * @typedef {Object} module:crypto~OlmSessionResult * @property {module:crypto/deviceinfo} device device info @@ -771,7 +752,7 @@ Crypto.prototype.importRoomKeys = function(keys) { }, ); }; -/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307 + /** * Encrypt an event according to the configuration of the room. * @@ -782,8 +763,7 @@ Crypto.prototype.importRoomKeys = function(keys) { * @return {module:client.Promise?} Promise which resolves when the event has been * encrypted, or null if nothing was needed */ -/* eslint-enable valid-jsdoc */ -Crypto.prototype.encryptEvent = async function(event, room) { +Crypto.prototype.encryptEvent = function(event, room) { if (!room) { throw new Error("Cannot send encrypted messages in unknown rooms"); } @@ -801,12 +781,6 @@ Crypto.prototype.encryptEvent = async function(event, room) { ); } - if (!this._roomDeviceTrackingState[roomId]) { - this.trackRoomDevices(roomId); - } - // wait for all the room devices to be loaded - await this._roomDeviceTrackingState[roomId]; - let content = event.getContent(); // If event has an m.relates_to then we need // to put this on the wrapping event instead @@ -817,19 +791,20 @@ Crypto.prototype.encryptEvent = async function(event, room) { delete content['m.relates_to']; } - const encryptedContent = await alg.encryptMessage( - room, event.getType(), content); + return alg.encryptMessage( + room, event.getType(), content, + ).then((encryptedContent) => { + if (mRelatesTo) { + encryptedContent['m.relates_to'] = mRelatesTo; + } - if (mRelatesTo) { - encryptedContent['m.relates_to'] = mRelatesTo; - } - - event.makeEncrypted( - "m.room.encrypted", - encryptedContent, - this._olmDevice.deviceCurve25519Key, - this._olmDevice.deviceEd25519Key, - ); + event.makeEncrypted( + "m.room.encrypted", + encryptedContent, + this._olmDevice.deviceCurve25519Key, + this._olmDevice.deviceEd25519Key, + ); + }); }; /** @@ -922,7 +897,9 @@ Crypto.prototype.onCryptoEvent = async function(event) { const content = event.getContent(); try { - await this.setRoomEncryption(roomId, content); + // inhibit the device list refresh for now - it will happen once we've + // finished processing the sync, in onSyncCompleted. + await this.setRoomEncryption(roomId, content, true); } catch (e) { console.error("Error configuring encryption in room " + roomId + ":", e); @@ -942,7 +919,6 @@ Crypto.prototype.onSyncWillProcess = async function(syncData) { // at which point we'll start tracking all the users of that room. console.log("Initial sync performed - resetting device tracking state"); this._deviceList.stopTrackingAllDeviceLists(); - this._roomDeviceTrackingState = {}; } }; @@ -988,12 +964,11 @@ Crypto.prototype._evalDeviceListChanges = async function(deviceLists) { }); } - if (deviceLists.left && Array.isArray(deviceLists.left) && - deviceLists.left.length) { + if (deviceLists.left && Array.isArray(deviceLists.left)) { // Check we really don't share any rooms with these users // any more: the server isn't required to give us the // exact correct set. - const e2eUserIds = new Set(await this._getTrackedE2eUsers()); + const e2eUserIds = new Set(await this._getE2eUsers()); deviceLists.left.forEach((u) => { if (!e2eUserIds.has(u)) { @@ -1005,13 +980,12 @@ Crypto.prototype._evalDeviceListChanges = async function(deviceLists) { /** * Get a list of all the IDs of users we share an e2e room with - * for which we are tracking devices already * * @returns {string[]} List of user IDs */ -Crypto.prototype._getTrackedE2eUsers = async function() { +Crypto.prototype._getE2eUsers = async function() { const e2eUserIds = []; - for (const room of this._getTrackedE2eRooms()) { + for (const room of this._getE2eRooms()) { const members = await room.getEncryptionTargetMembers(); for (const member of members) { e2eUserIds.push(member.userId); @@ -1021,21 +995,17 @@ Crypto.prototype._getTrackedE2eUsers = async function() { }; /** - * Get a list of the e2e-enabled rooms we are members of, - * and for which we are already tracking the devices + * Get a list of the e2e-enabled rooms we are members of * * @returns {module:models.Room[]} */ -Crypto.prototype._getTrackedE2eRooms = function() { +Crypto.prototype._getE2eRooms = function() { return this._clientStore.getRooms().filter((room) => { // check for rooms with encryption enabled const alg = this._roomEncryptors[room.roomId]; if (!alg) { return false; } - if (!this._roomDeviceTrackingState[room.roomId]) { - return false; - } // ignore any rooms which we have left const myMembership = room.getMyMembership(); @@ -1104,20 +1074,15 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) { // not encrypting in this room return; } - // only mark users in this room as tracked if we already started tracking in this room - // this way we don't start device queries after sync on behalf of this room which we won't use - // the result of anyway, as we'll need to do a query again once all the members are fetched - // by calling _trackRoomDevices - if (this._roomDeviceTrackingState[roomId]) { - if (member.membership == 'join') { - console.log('Join event for ' + member.userId + ' in ' + roomId); - // make sure we are tracking the deviceList for this user - this._deviceList.startTrackingDeviceList(member.userId); - } else if (member.membership == 'invite' && - this._clientStore.getRoom(roomId).shouldEncryptForInvitedMembers()) { - console.log('Invite event for ' + member.userId + ' in ' + roomId); - this._deviceList.startTrackingDeviceList(member.userId); - } + + if (member.membership == 'join') { + console.log('Join event for ' + member.userId + ' in ' + roomId); + // make sure we are tracking the deviceList for this user + this._deviceList.startTrackingDeviceList(member.userId); + } else if (member.membership == 'invite' && + this._clientStore.getRoom(roomId).shouldEncryptForInvitedMembers()) { + console.log('Invite event for ' + member.userId + ' in ' + roomId); + this._deviceList.startTrackingDeviceList(member.userId); } alg.onRoomMembership(event, member, oldMembership); diff --git a/src/models/room.js b/src/models/room.js index dc1052ea4..94115ef45 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -368,11 +368,6 @@ Room.prototype.loadMembersIfNeeded = function() { }); this._membersPromise = promise; - // 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 this._membersPromise; }; diff --git a/src/sync.js b/src/sync.js index d8dffe6b3..4aa2d2482 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1085,16 +1085,15 @@ SyncApi.prototype._processSyncResponse = async function( self._processEventsForNotifs(room, timelineEvents); - function processRoomEvent(e) { + async function processRoomEvent(e) { client.emit("event", e); if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) { - self.opts.crypto.onCryptoEvent(e); + await self.opts.crypto.onCryptoEvent(e); } } - stateEvents.forEach(processRoomEvent); - timelineEvents.forEach(processRoomEvent); - + await Promise.mapSeries(stateEvents, processRoomEvent); + await Promise.mapSeries(timelineEvents, processRoomEvent); ephemeralEvents.forEach(function(e) { client.emit("event", e); }); From 60c9c403bd1ffe8abe780e7062d3f4711285d320 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 24 Aug 2018 17:49:57 +0200 Subject: [PATCH 167/472] 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. --- src/crypto/index.js | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 8b35bc0a4..3bc2b7843 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -619,20 +619,35 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) { Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDeviceQuery) { // if we already have encryption in this room, we should ignore this event // (for now at least. maybe we should alert the user somehow?) - const existingConfig = this._roomList.getRoomEncryption(roomId); - if (existingConfig && JSON.stringify(existingConfig) != JSON.stringify(config)) { - console.error("Ignoring m.room.encryption event which requests " + - "a change of config in " + roomId); + const existingAlg = this._roomEncryptors[roomId]; + if (existingAlg) { return; } + // _roomList.getRoomEncryption will not race with _roomList.setRoomEncryption + // because it first stores in memory. We should await the promise only + // after all the in-memory state (_roomEncryptors and _roomList) has been updated + // to avoid races when calling this method multiple times. Hence keep a hold of the promise. + let storeConfigPromise; + // if state is being replayed from storage, we might already have a configuration + // for this room. We just need to make sure the algorithm in + // _roomEncryptors and config in _roomList are in sync + // by making sure the existingConfig is identical to config. + const existingConfig = this._roomList.getRoomEncryption(roomId); + if (existingConfig) { + if (JSON.stringify(existingConfig) != JSON.stringify(config)) { + console.error("Ignoring m.room.encryption event which requests " + + "a change of config in " + roomId); + return; + } + } else { + const storeConfigPromise = this._roomList.setRoomEncryption(roomId, config); + } const AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm]; if (!AlgClass) { throw new Error("Unable to encrypt with " + config.algorithm); } - await this._roomList.setRoomEncryption(roomId, config); - const alg = new AlgClass({ userId: this._userId, deviceId: this._deviceId, @@ -644,6 +659,10 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic }); this._roomEncryptors[roomId] = alg; + if (storeConfigPromise) { + await storeConfigPromise; + } + // make sure we are tracking the device lists for all users in this room. console.log("Enabling encryption in " + roomId + "; " + "starting to track device lists for all users therein"); From 0028bfbfc743e373111235788fa2617923a776e3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 24 Aug 2018 18:42:48 +0200 Subject: [PATCH 168/472] fix lint --- src/crypto/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 3bc2b7843..47662c257 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -627,7 +627,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic // because it first stores in memory. We should await the promise only // after all the in-memory state (_roomEncryptors and _roomList) has been updated // to avoid races when calling this method multiple times. Hence keep a hold of the promise. - let storeConfigPromise; + let storeConfigPromise = null; // if state is being replayed from storage, we might already have a configuration // for this room. We just need to make sure the algorithm in // _roomEncryptors and config in _roomList are in sync From 017f81e430cbb36f82ea78d6d1f4f7de24d5d6f9 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 24 Aug 2018 16:39:22 -0400 Subject: [PATCH 169/472] fix some bugs --- src/client.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 926b09355..a434b67da 100644 --- a/src/client.js +++ b/src/client.js @@ -41,6 +41,7 @@ const SyncApi = require("./sync"); const MatrixBaseApis = require("./base-apis"); const MatrixError = httpApi.MatrixError; const ContentHelpers = require("./content-helpers"); +const olmlib = require("./crypto/olmlib"); import ReEmitter from './ReEmitter'; import RoomList from './crypto/RoomList'; @@ -782,7 +783,9 @@ MatrixClient.prototype.createKeyBackupVersion = function(callback) { // FIXME: pickle isn't the right thing to use, but we don't have // anything else yet const recovery_key = decryption.pickle("secret_key"); - callback(null, recovery_key); + if (callback) { + callback(null, recovery_key); + } return recovery_key; }); } From bf873bde42b1c59af496e9590b03109b1c51ac5e Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 24 Aug 2018 22:13:13 -0400 Subject: [PATCH 170/472] split the backup version creation into two different methods --- src/client.js | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/client.js b/src/client.js index a434b67da..3e0f8a5cc 100644 --- a/src/client.js +++ b/src/client.js @@ -758,35 +758,49 @@ MatrixClient.prototype.disableKeyBackup = function() { } /** - * Create a new key backup version and enable it. + * Set up the data required to create a new backup version. The backup version + * will not be created and enabled until createKeyBackupVersion is called. */ -MatrixClient.prototype.createKeyBackupVersion = function(callback) { +MatrixClient.prototype.prepareKeyBackupVersion = function(callback) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } const decryption = new global.Olm.PkDecryption(); const public_key = decryption.generate_key(); - const encryption = new global.Olm.PkEncryption(); - encryption.set_recipient_key(public_key); - const data = { + return { algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, auth_data: { public_key: public_key, - } + }, + // FIXME: pickle isn't the right thing to use, but we don't have + // anything else yet, so use it for now + recovery_key: decryption.pickle("secret_key"), }; +} + +/** + * Create a new key backup version and enable it, using the information return + * from prepareKeyBackupVersion. + */ +MatrixClient.prototype.createKeyBackupVersion = function(info, callback) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const data = { + algorithm: info.algorithm, + auth_data: info.auth_data, // FIXME: should this be cloned? + } this._crypto._signObject(data.auth_data); return this._http.authedRequest( undefined, "POST", "/room_keys/version", undefined, data, ).then((res) => { - this._crypto.backupKey = encryption; - // FIXME: pickle isn't the right thing to use, but we don't have - // anything else yet - const recovery_key = decryption.pickle("secret_key"); + this.enableKeyBackup(info); if (callback) { - callback(null, recovery_key); + callback(null, res); } - return recovery_key; + return res; }); } From ae7e90dc2faeb7688e5f60ca8ffa6edd13af4170 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 27 Aug 2018 10:39:14 +0200 Subject: [PATCH 171/472] do config comparison first to keep original error message --- src/crypto/index.js | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 47662c257..08012e420 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -617,21 +617,11 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) { * users in the room (for now) */ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDeviceQuery) { - // if we already have encryption in this room, we should ignore this event - // (for now at least. maybe we should alert the user somehow?) - const existingAlg = this._roomEncryptors[roomId]; - if (existingAlg) { - return; - } - // _roomList.getRoomEncryption will not race with _roomList.setRoomEncryption - // because it first stores in memory. We should await the promise only - // after all the in-memory state (_roomEncryptors and _roomList) has been updated - // to avoid races when calling this method multiple times. Hence keep a hold of the promise. - let storeConfigPromise = null; // if state is being replayed from storage, we might already have a configuration - // for this room. We just need to make sure the algorithm in - // _roomEncryptors and config in _roomList are in sync - // by making sure the existingConfig is identical to config. + // for this room as they are persisted as well. + // We just need to make sure the algorithm is initialized in this case. + // However, if the new config is different, + // we should bail out as room encryption can't be changed once set. const existingConfig = this._roomList.getRoomEncryption(roomId); if (existingConfig) { if (JSON.stringify(existingConfig) != JSON.stringify(config)) { @@ -639,7 +629,25 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic "a change of config in " + roomId); return; } - } else { + } + // if we already have encryption in this room, we should ignore this event, + // as it would reset the encryption algorithm. + // This is at least expected to be called twice, as sync calls onCryptoEvent + // for both the timeline and state sections in the /sync response, + // the encryption event would appear in both. + // If it's called more than twice though, + // it signals a bug on client or server. + const existingAlg = this._roomEncryptors[roomId]; + if (existingAlg) { + return; + } + + // _roomList.getRoomEncryption will not race with _roomList.setRoomEncryption + // because it first stores in memory. We should await the promise only + // after all the in-memory state (_roomEncryptors and _roomList) has been updated + // to avoid races when calling this method multiple times. Hence keep a hold of the promise. + let storeConfigPromise = null; + if(!existingConfig) { const storeConfigPromise = this._roomList.setRoomEncryption(roomId, config); } From 78b08bfef25fde591559deb1f9433f7ed2439cba Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 27 Aug 2018 10:48:11 +0200 Subject: [PATCH 172/472] fix var declaration --- src/crypto/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 08012e420..77268cf2c 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -648,7 +648,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic // to avoid races when calling this method multiple times. Hence keep a hold of the promise. let storeConfigPromise = null; if(!existingConfig) { - const storeConfigPromise = this._roomList.setRoomEncryption(roomId, config); + storeConfigPromise = this._roomList.setRoomEncryption(roomId, config); } const AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm]; From 21e0c79f7d8f447b79cf3e334ae54b4c7bf17d71 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 24 Aug 2018 10:55:59 +0200 Subject: [PATCH 173/472] Revert "Revert "Lazy loading: don't block on setting up room crypto"" This reverts commit 5cf2ebea4fc2728e5492c108b04120daa81bc610. --- spec/unit/room.spec.js | 3 + src/crypto/RoomList.js | 3 + src/crypto/index.js | 138 +++++++++++++++++++++++++---------------- src/models/room.js | 5 ++ src/sync.js | 9 +-- 5 files changed, 102 insertions(+), 56 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index eb5ba5a51..b98559ecf 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1318,6 +1318,9 @@ describe("Room", function() { // events should already be MatrixEvents return function(event) {return event;}; }, + isRoomEncrypted: function() { + return false; + }, _http: { serverResponse, authedRequest: function() { diff --git a/src/crypto/RoomList.js b/src/crypto/RoomList.js index 5bb437fb7..eb5b469c6 100644 --- a/src/crypto/RoomList.js +++ b/src/crypto/RoomList.js @@ -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 for calling + // this twice for consecutive m.room.encryption events this._roomEncryption[roomId] = roomInfo; await this._cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => { diff --git a/src/crypto/index.js b/src/crypto/index.js index 77268cf2c..756fdb3e9 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -106,6 +106,10 @@ function Crypto(baseApis, sessionStore, userId, deviceId, this._receivedRoomKeyRequestCancellations = []; // true if we are currently processing received room key requests this._processingRoomKeyRequests = false; + // track if an initial tracking of all the room members + // has happened for a given room. This is delayed + // to avoid loading room members as long as possible. + this._roomDeviceTrackingState = {}; } utils.inherits(Crypto, EventEmitter); @@ -612,9 +616,6 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) { * @param {string} roomId The room ID to enable encryption in. * * @param {object} config The encryption config for the room. - * - * @param {boolean=} inhibitDeviceQuery true to suppress device list query for - * users in the room (for now) */ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDeviceQuery) { // if state is being replayed from storage, we might already have a configuration @@ -670,25 +671,42 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic if (storeConfigPromise) { await storeConfigPromise; } - - // make sure we are tracking the device lists for all users in this room. - console.log("Enabling encryption in " + roomId + "; " + - "starting to track device lists for all users therein"); - const room = this._clientStore.getRoom(roomId); - if (!room) { - throw new Error(`Unable to enable encryption in unknown room ${roomId}`); - } - - const members = await room.getEncryptionTargetMembers(); - members.forEach((m) => { - this._deviceList.startTrackingDeviceList(m.userId); - }); - if (!inhibitDeviceQuery) { - this._deviceList.refreshOutdatedDeviceLists(); - } + console.log("Enabling encryption in " + roomId); }; +/** + * Make sure we are tracking the device lists for all users in this room. + * + * @param {string} roomId The room ID to start tracking devices in. + * @returns {Promise} when all devices for the room have been fetched and marked to track + */ +Crypto.prototype.trackRoomDevices = function(roomId) { + const trackAndRefresh = async () => { + // not an encrypted room + if (!this._roomEncryptors[roomId]) { + return; + } + const room = this._clientStore.getRoom(roomId); + if (!room) { + throw new Error(`Unable to start tracking devices in unknown room ${roomId}`); + } + console.log(`Starting to track devices for room ${roomId} ...`); + const members = await room.getEncryptionTargetMembers(); + members.forEach((m) => { + this._deviceList.startTrackingDeviceList(m.userId); + }); + return this._deviceList.refreshOutdatedDeviceLists(); + }; + + let promise = this._roomDeviceTrackingState[roomId]; + if (!promise) { + promise = trackAndRefresh(); + this._roomDeviceTrackingState[roomId] = promise; + } + return promise; +}; + /** * @typedef {Object} module:crypto~OlmSessionResult * @property {module:crypto/deviceinfo} device device info @@ -779,7 +797,7 @@ Crypto.prototype.importRoomKeys = function(keys) { }, ); }; - +/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307 /** * Encrypt an event according to the configuration of the room. * @@ -790,7 +808,8 @@ Crypto.prototype.importRoomKeys = function(keys) { * @return {module:client.Promise?} Promise which resolves when the event has been * encrypted, or null if nothing was needed */ -Crypto.prototype.encryptEvent = function(event, room) { +/* eslint-enable valid-jsdoc */ +Crypto.prototype.encryptEvent = async function(event, room) { if (!room) { throw new Error("Cannot send encrypted messages in unknown rooms"); } @@ -808,6 +827,12 @@ Crypto.prototype.encryptEvent = function(event, room) { ); } + if (!this._roomDeviceTrackingState[roomId]) { + this.trackRoomDevices(roomId); + } + // wait for all the room devices to be loaded + await this._roomDeviceTrackingState[roomId]; + let content = event.getContent(); // If event has an m.relates_to then we need // to put this on the wrapping event instead @@ -818,20 +843,19 @@ Crypto.prototype.encryptEvent = function(event, room) { delete content['m.relates_to']; } - return alg.encryptMessage( - room, event.getType(), content, - ).then((encryptedContent) => { - if (mRelatesTo) { - encryptedContent['m.relates_to'] = mRelatesTo; - } + const encryptedContent = await alg.encryptMessage( + room, event.getType(), content); - event.makeEncrypted( - "m.room.encrypted", - encryptedContent, - this._olmDevice.deviceCurve25519Key, - this._olmDevice.deviceEd25519Key, - ); - }); + if (mRelatesTo) { + encryptedContent['m.relates_to'] = mRelatesTo; + } + + event.makeEncrypted( + "m.room.encrypted", + encryptedContent, + this._olmDevice.deviceCurve25519Key, + this._olmDevice.deviceEd25519Key, + ); }; /** @@ -924,9 +948,7 @@ Crypto.prototype.onCryptoEvent = async function(event) { const content = event.getContent(); try { - // inhibit the device list refresh for now - it will happen once we've - // finished processing the sync, in onSyncCompleted. - await this.setRoomEncryption(roomId, content, true); + await this.setRoomEncryption(roomId, content); } catch (e) { console.error("Error configuring encryption in room " + roomId + ":", e); @@ -946,6 +968,7 @@ Crypto.prototype.onSyncWillProcess = async function(syncData) { // at which point we'll start tracking all the users of that room. console.log("Initial sync performed - resetting device tracking state"); this._deviceList.stopTrackingAllDeviceLists(); + this._roomDeviceTrackingState = {}; } }; @@ -991,11 +1014,12 @@ Crypto.prototype._evalDeviceListChanges = async function(deviceLists) { }); } - if (deviceLists.left && Array.isArray(deviceLists.left)) { + if (deviceLists.left && Array.isArray(deviceLists.left) && + deviceLists.left.length) { // Check we really don't share any rooms with these users // any more: the server isn't required to give us the // exact correct set. - const e2eUserIds = new Set(await this._getE2eUsers()); + const e2eUserIds = new Set(await this._getTrackedE2eUsers()); deviceLists.left.forEach((u) => { if (!e2eUserIds.has(u)) { @@ -1007,12 +1031,13 @@ Crypto.prototype._evalDeviceListChanges = async function(deviceLists) { /** * Get a list of all the IDs of users we share an e2e room with + * for which we are tracking devices already * * @returns {string[]} List of user IDs */ -Crypto.prototype._getE2eUsers = async function() { +Crypto.prototype._getTrackedE2eUsers = async function() { const e2eUserIds = []; - for (const room of this._getE2eRooms()) { + for (const room of this._getTrackedE2eRooms()) { const members = await room.getEncryptionTargetMembers(); for (const member of members) { e2eUserIds.push(member.userId); @@ -1022,17 +1047,21 @@ Crypto.prototype._getE2eUsers = async function() { }; /** - * Get a list of the e2e-enabled rooms we are members of + * Get a list of the e2e-enabled rooms we are members of, + * and for which we are already tracking the devices * * @returns {module:models.Room[]} */ -Crypto.prototype._getE2eRooms = function() { +Crypto.prototype._getTrackedE2eRooms = function() { return this._clientStore.getRooms().filter((room) => { // check for rooms with encryption enabled const alg = this._roomEncryptors[room.roomId]; if (!alg) { return false; } + if (!this._roomDeviceTrackingState[room.roomId]) { + return false; + } // ignore any rooms which we have left const myMembership = room.getMyMembership(); @@ -1101,15 +1130,20 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) { // not encrypting in this room return; } - - if (member.membership == 'join') { - console.log('Join event for ' + member.userId + ' in ' + roomId); - // make sure we are tracking the deviceList for this user - this._deviceList.startTrackingDeviceList(member.userId); - } else if (member.membership == 'invite' && - this._clientStore.getRoom(roomId).shouldEncryptForInvitedMembers()) { - console.log('Invite event for ' + member.userId + ' in ' + roomId); - this._deviceList.startTrackingDeviceList(member.userId); + // only mark users in this room as tracked if we already started tracking in this room + // this way we don't start device queries after sync on behalf of this room which we won't use + // the result of anyway, as we'll need to do a query again once all the members are fetched + // by calling _trackRoomDevices + if (this._roomDeviceTrackingState[roomId]) { + if (member.membership == 'join') { + console.log('Join event for ' + member.userId + ' in ' + roomId); + // make sure we are tracking the deviceList for this user + this._deviceList.startTrackingDeviceList(member.userId); + } else if (member.membership == 'invite' && + this._clientStore.getRoom(roomId).shouldEncryptForInvitedMembers()) { + console.log('Invite event for ' + member.userId + ' in ' + roomId); + this._deviceList.startTrackingDeviceList(member.userId); + } } alg.onRoomMembership(event, member, oldMembership); diff --git a/src/models/room.js b/src/models/room.js index 94115ef45..dc1052ea4 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -368,6 +368,11 @@ Room.prototype.loadMembersIfNeeded = function() { }); this._membersPromise = promise; + // 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 this._membersPromise; }; diff --git a/src/sync.js b/src/sync.js index 4aa2d2482..d8dffe6b3 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1085,15 +1085,16 @@ SyncApi.prototype._processSyncResponse = async function( self._processEventsForNotifs(room, timelineEvents); - async function processRoomEvent(e) { + function processRoomEvent(e) { client.emit("event", e); if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) { - await self.opts.crypto.onCryptoEvent(e); + self.opts.crypto.onCryptoEvent(e); } } - await Promise.mapSeries(stateEvents, processRoomEvent); - await Promise.mapSeries(timelineEvents, processRoomEvent); + stateEvents.forEach(processRoomEvent); + timelineEvents.forEach(processRoomEvent); + ephemeralEvents.forEach(function(e) { client.emit("event", e); }); From 7247762b60fba020003585a55a16e0e9029fc127 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 23 Aug 2018 18:44:06 +0200 Subject: [PATCH 174/472] Also support not lazy-loading members in Crypto --- src/client.js | 4 ++++ src/crypto/index.js | 35 +++++++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/client.js b/src/client.js index 54f9647f2..1f3a48130 100644 --- a/src/client.js +++ b/src/client.js @@ -3098,6 +3098,10 @@ MatrixClient.prototype.startClient = async function(opts) { } } + if (opts.lazyLoadMembers && this._crypto) { + this._crypto.enableLazyLoading(); + } + opts.crypto = this._crypto; opts.canResetEntireTimeline = (roomId) => { if (!this._canResetTimelineCallback) { diff --git a/src/crypto/index.js b/src/crypto/index.js index 756fdb3e9..4f414314a 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -106,6 +106,11 @@ function Crypto(baseApis, sessionStore, userId, deviceId, this._receivedRoomKeyRequestCancellations = []; // true if we are currently processing received room key requests this._processingRoomKeyRequests = false; + // controls whether device tracking is delayed + // until calling encryptEvent or trackRoomDevices, + // or done immediately upon enabling room encryption. + this._lazyLoadMembers = false; + // in case _lazyLoadMembers is true, // track if an initial tracking of all the room members // has happened for a given room. This is delayed // to avoid loading room members as long as possible. @@ -171,6 +176,15 @@ Crypto.prototype.init = async function() { } }; + + + +/** + */ +Crypto.prototype.enableLazyLoading = function() { + this._lazyLoadMembers = true; +}; + /** * Tell the crypto module to register for MatrixClient events which it needs to * listen for @@ -672,6 +686,13 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic await storeConfigPromise; } console.log("Enabling encryption in " + roomId); + + if (!this._lazyLoadMembers) { + const members = await room.getEncryptionTargetMembers(); + members.forEach((m) => { + this._deviceList.startTrackingDeviceList(m.userId); + }); + } }; @@ -827,11 +848,13 @@ Crypto.prototype.encryptEvent = async function(event, room) { ); } - if (!this._roomDeviceTrackingState[roomId]) { - this.trackRoomDevices(roomId); + if (this._lazyLoadMembers) { + if (!this._roomDeviceTrackingState[roomId]) { + this.trackRoomDevices(roomId); + } + // wait for all the room devices to be loaded + await this._roomDeviceTrackingState[roomId]; } - // wait for all the room devices to be loaded - await this._roomDeviceTrackingState[roomId]; let content = event.getContent(); // If event has an m.relates_to then we need @@ -1059,7 +1082,7 @@ Crypto.prototype._getTrackedE2eRooms = function() { if (!alg) { return false; } - if (!this._roomDeviceTrackingState[room.roomId]) { + if (this._lazyLoadMembers && !this._roomDeviceTrackingState[room.roomId]) { return false; } @@ -1134,7 +1157,7 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) { // this way we don't start device queries after sync on behalf of this room which we won't use // the result of anyway, as we'll need to do a query again once all the members are fetched // by calling _trackRoomDevices - if (this._roomDeviceTrackingState[roomId]) { + if (!this._lazyLoadMembers || this._roomDeviceTrackingState[roomId]) { if (member.membership == 'join') { console.log('Join event for ' + member.userId + ' in ' + roomId); // make sure we are tracking the deviceList for this user From 5e5994f1669e658b943b9a811aac43b5ef805b0b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 23 Aug 2018 19:49:23 +0200 Subject: [PATCH 175/472] try and fix tests --- spec/integ/devicelist-integ-spec.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/integ/devicelist-integ-spec.js b/spec/integ/devicelist-integ-spec.js index f3f889deb..b6ed84267 100644 --- a/spec/integ/devicelist-integ-spec.js +++ b/spec/integ/devicelist-integ-spec.js @@ -331,8 +331,8 @@ describe("DeviceList management:", function() { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; - expect(bobStat).toEqual( - 0, "Alice should have marked bob's device list as untracked", + expect(bobStat).toBeFalsy( //0 or undefined + "Alice should have marked bob's device list as untracked", ); }); }); @@ -367,8 +367,8 @@ describe("DeviceList management:", function() { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; - expect(bobStat).toEqual( - 0, "Alice should have marked bob's device list as untracked", + expect(bobStat).toBeFalsy( //0 or undefined + "Alice should have marked bob's device list as untracked", ); }); }); @@ -388,8 +388,8 @@ describe("DeviceList management:", function() { anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; - expect(bobStat).toEqual( - 0, "Alice should have marked bob's device list as untracked", + expect(bobStat).toBeFalsy( //0 or undefined + "Alice should have marked bob's device list as untracked", ); }); } finally { From 7d00c0bd5a9de739eec80ae812464198b7deb3db Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 23 Aug 2018 23:38:53 +0200 Subject: [PATCH 176/472] make LL/non-LL flow in Crypto more alike by always going through _roomDeviceTrackingState --- src/crypto/index.js | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 4f414314a..cbc9318d4 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -688,10 +688,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic console.log("Enabling encryption in " + roomId); if (!this._lazyLoadMembers) { - const members = await room.getEncryptionTargetMembers(); - members.forEach((m) => { - this._deviceList.startTrackingDeviceList(m.userId); - }); + await this.trackRoomDevices(roomId); } }; @@ -717,7 +714,6 @@ Crypto.prototype.trackRoomDevices = function(roomId) { members.forEach((m) => { this._deviceList.startTrackingDeviceList(m.userId); }); - return this._deviceList.refreshOutdatedDeviceLists(); }; let promise = this._roomDeviceTrackingState[roomId]; @@ -848,13 +844,11 @@ Crypto.prototype.encryptEvent = async function(event, room) { ); } - if (this._lazyLoadMembers) { - if (!this._roomDeviceTrackingState[roomId]) { - this.trackRoomDevices(roomId); - } - // wait for all the room devices to be loaded - await this._roomDeviceTrackingState[roomId]; + if (!this._roomDeviceTrackingState[roomId]) { + this.trackRoomDevices(roomId); } + // wait for all the room devices to be loaded + await this._roomDeviceTrackingState[roomId]; let content = event.getContent(); // If event has an m.relates_to then we need @@ -1082,7 +1076,7 @@ Crypto.prototype._getTrackedE2eRooms = function() { if (!alg) { return false; } - if (this._lazyLoadMembers && !this._roomDeviceTrackingState[room.roomId]) { + if (!this._roomDeviceTrackingState[room.roomId]) { return false; } @@ -1157,7 +1151,7 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) { // this way we don't start device queries after sync on behalf of this room which we won't use // the result of anyway, as we'll need to do a query again once all the members are fetched // by calling _trackRoomDevices - if (!this._lazyLoadMembers || this._roomDeviceTrackingState[roomId]) { + if (this._roomDeviceTrackingState[roomId]) { if (member.membership == 'join') { console.log('Join event for ' + member.userId + ' in ' + roomId); // make sure we are tracking the deviceList for this user From 362bf1895dde40776dc9f15e66ad5c8ded5dbd56 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 27 Aug 2018 11:12:00 +0200 Subject: [PATCH 177/472] restore inhibitDeviceQuery param to avoid breaking change --- src/crypto/index.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index cbc9318d4..43e092c72 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -176,9 +176,6 @@ Crypto.prototype.init = async function() { } }; - - - /** */ Crypto.prototype.enableLazyLoading = function() { @@ -630,6 +627,10 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) { * @param {string} roomId The room ID to enable encryption in. * * @param {object} config The encryption config for the room. + * + * @param {boolean=} inhibitDeviceQuery true to suppress device list query for + * users in the room (for now). In case lazy loading is enabled, + * the device query is always inhibited as the members are not tracked. */ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDeviceQuery) { // if state is being replayed from storage, we might already have a configuration @@ -685,10 +686,21 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic if (storeConfigPromise) { await storeConfigPromise; } - console.log("Enabling encryption in " + roomId); if (!this._lazyLoadMembers) { + console.log("Enabling encryption in " + roomId + "; " + + "starting to track device lists for all users therein"); + await this.trackRoomDevices(roomId); + // TODO: this flag is only not used from MatrixClient::setRoomEncryption + // which is never used (inside riot at least) + // but didn't want to remove it as it technically would + // be a breaking change. + if(!this.inhibitDeviceQuery) { + this._deviceList.refreshOutdatedDeviceLists(); + } + } else { + console.log("Enabling encryption in " + roomId); } }; @@ -700,7 +712,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic * @returns {Promise} when all devices for the room have been fetched and marked to track */ Crypto.prototype.trackRoomDevices = function(roomId) { - const trackAndRefresh = async () => { + const trackMembers = async () => { // not an encrypted room if (!this._roomEncryptors[roomId]) { return; @@ -718,7 +730,7 @@ Crypto.prototype.trackRoomDevices = function(roomId) { let promise = this._roomDeviceTrackingState[roomId]; if (!promise) { - promise = trackAndRefresh(); + promise = trackMembers(); this._roomDeviceTrackingState[roomId] = promise; } return promise; @@ -965,7 +977,7 @@ Crypto.prototype.onCryptoEvent = async function(event) { const content = event.getContent(); try { - await this.setRoomEncryption(roomId, content); + await this.setRoomEncryption(roomId, content, true); } catch (e) { console.error("Error configuring encryption in room " + roomId + ":", e); From ad71bb30acc380ea03a38b972da5a3e4ae1aca8a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 27 Aug 2018 11:51:29 +0200 Subject: [PATCH 178/472] add comment back as we kept flag in the end --- src/crypto/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/crypto/index.js b/src/crypto/index.js index 43e092c72..4f6dbd24e 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -977,6 +977,8 @@ Crypto.prototype.onCryptoEvent = async function(event) { const content = event.getContent(); try { + // inhibit the device list refresh for now - it will happen once we've + // finished processing the sync, in onSyncCompleted. await this.setRoomEncryption(roomId, content, true); } catch (e) { console.error("Error configuring encryption in room " + roomId + From 4e25867548886582e8dd45dbc69c13919ae21963 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 27 Aug 2018 11:56:46 +0200 Subject: [PATCH 179/472] 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 --- src/sync.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sync.js b/src/sync.js index d8dffe6b3..ac6c6272b 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1085,15 +1085,15 @@ SyncApi.prototype._processSyncResponse = async function( self._processEventsForNotifs(room, timelineEvents); - function processRoomEvent(e) { + async function processRoomEvent(e) { client.emit("event", e); if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) { - self.opts.crypto.onCryptoEvent(e); + await self.opts.crypto.onCryptoEvent(e); } } - stateEvents.forEach(processRoomEvent); - timelineEvents.forEach(processRoomEvent); + await Promise.mapSeries(stateEvents, processRoomEvent); + await Promise.mapSeries(timelineEvents, processRoomEvent); ephemeralEvents.forEach(function(e) { client.emit("event", e); From c47445ca987feb340f78b1d91185b731d4c6549a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 27 Aug 2018 12:01:22 +0200 Subject: [PATCH 180/472] no need to just add a space now --- src/sync.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sync.js b/src/sync.js index ac6c6272b..4aa2d2482 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1094,7 +1094,6 @@ SyncApi.prototype._processSyncResponse = async function( await Promise.mapSeries(stateEvents, processRoomEvent); await Promise.mapSeries(timelineEvents, processRoomEvent); - ephemeralEvents.forEach(function(e) { client.emit("event", e); }); From 85f1da1f10359ac641374f49ad66b2f53857c8fa Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 27 Aug 2018 12:09:10 +0200 Subject: [PATCH 181/472] revert unnecesary changes --- spec/integ/devicelist-integ-spec.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/integ/devicelist-integ-spec.js b/spec/integ/devicelist-integ-spec.js index b6ed84267..f3f889deb 100644 --- a/spec/integ/devicelist-integ-spec.js +++ b/spec/integ/devicelist-integ-spec.js @@ -331,8 +331,8 @@ describe("DeviceList management:", function() { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; - expect(bobStat).toBeFalsy( //0 or undefined - "Alice should have marked bob's device list as untracked", + expect(bobStat).toEqual( + 0, "Alice should have marked bob's device list as untracked", ); }); }); @@ -367,8 +367,8 @@ describe("DeviceList management:", function() { aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; - expect(bobStat).toBeFalsy( //0 or undefined - "Alice should have marked bob's device list as untracked", + expect(bobStat).toEqual( + 0, "Alice should have marked bob's device list as untracked", ); }); }); @@ -388,8 +388,8 @@ describe("DeviceList management:", function() { anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => { const bobStat = data.trackingStatus['@bob:xyz']; - expect(bobStat).toBeFalsy( //0 or undefined - "Alice should have marked bob's device list as untracked", + expect(bobStat).toEqual( + 0, "Alice should have marked bob's device list as untracked", ); }); } finally { From b0b0291bc752a3a629433030a415154984e77c10 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 28 Aug 2018 01:27:20 +0100 Subject: [PATCH 182/472] hopefully fix invite_room_state as per https://github.com/vector-im/riot-web/issues/7229 --- src/models/room.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/models/room.js b/src/models/room.js index 94115ef45..acac0ef4d 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1141,7 +1141,10 @@ Room.prototype.recalculate = function() { "m.room.member", this.myUserId, ); if (membershipEvent && membershipEvent.getContent().membership === "invite") { - const strippedStateEvents = membershipEvent.event.invite_room_state || []; + const strippedStateEvents = ( + membershipEvent.event.unsigned ? + membershipEvent.event.unsigned.invite_room_state : + []) || []; utils.forEach(strippedStateEvents, function(strippedEvent) { const existingEvent = self.currentState.getStateEvents( strippedEvent.type, strippedEvent.state_key, From a6ebfe4215633bc2dcff56755a14d7ba3ea1c358 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 28 Aug 2018 15:31:20 +0200 Subject: [PATCH 183/472] typo --- src/crypto/RoomList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/RoomList.js b/src/crypto/RoomList.js index eb5b469c6..1f0234548 100644 --- a/src/crypto/RoomList.js +++ b/src/crypto/RoomList.js @@ -72,7 +72,7 @@ export default class RoomList { async setRoomEncryption(roomId, roomInfo) { // important that this happens before calling into the store - // as it prevents the Crypto::setRoomEncryption for calling + // as it prevents the Crypto::setRoomEncryption from calling // this twice for consecutive m.room.encryption events this._roomEncryption[roomId] = roomInfo; await this._cryptoStore.doTxn( From a6de395cde89fd8be09f234bba80251012b77742 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 28 Aug 2018 18:03:59 +0100 Subject: [PATCH 184/472] unbreak tests from b0b0291bc752a3a629433030a415154984e77c10 --- spec/unit/room.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index b98559ecf..5f5f87a3f 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -667,7 +667,7 @@ describe("Room", function() { const roomName = "flibble"; const event = addMember(userA, "invite"); - event.event.invite_room_state = [ + event.event.unsigned.invite_room_state = [ { type: "m.room.name", state_key: "", @@ -686,7 +686,7 @@ describe("Room", function() { const roomName = "flibble"; setRoomName(roomName); const roomNameToIgnore = "ignoreme"; - event.event.invite_room_state = [ + event.event.unsigned.invite_room_state = [ { type: "m.room.name", state_key: "", From 1b77ee0ef43df1b3e9ca5f06e6f4ac4cce2f4716 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 29 Aug 2018 18:06:45 +0100 Subject: [PATCH 185/472] Pass through function to discard megolm session To make debugging crypto slightly faster --- src/client.js | 15 +++++++++++++++ src/crypto/algorithms/megolm.js | 11 +++++++++++ src/crypto/index.js | 17 +++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/src/client.js b/src/client.js index 1f3a48130..d345d9a44 100644 --- a/src/client.js +++ b/src/client.js @@ -702,6 +702,21 @@ MatrixClient.prototype.isRoomEncrypted = function(roomId) { return this._roomList.isRoomEncrypted(roomId); }; +/** + * Forces the current outbound group session to be discarded such + * that another one will be created next time an event is sent. + * + * @param roomId The ID of the room to discard the session for + * + * This should not normally be necessary. + */ +MatrixClient.prototype.forceDiscardSession = function(roomId) { + if (!this._crypto) { + throw new Error("End-to-End encryption disabled"); + } + return this._crypto.forceDiscardSession(roomId); +}; + /** * Get a list containing all of the room keys * diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index a4eb32ab0..46ae3398e 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -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. @@ -496,6 +497,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 diff --git a/src/crypto/index.js b/src/crypto/index.js index 4f6dbd24e..a8f4207c1 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -621,6 +621,23 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) { return device; }; +/** + * Forces the current outbound group session to be discarded such + * that another one will be created next time an event is sent. + * + * @param roomId The ID of the room to discard the session for + * + * This should not normally be necessary. + */ +Crypto.prototype.forceDiscardSession = function(roomId) { + const alg = this._roomEncryptors[roomId]; + if (alg === undefined) throw new Error("Room not encrypted"); + if (alg.forceDiscardSession === undefined) { + throw new Error("Room encryption algorithm doesn't support session discarding"); + } + alg.forceDiscardSession(); +}; + /** * Configure a room to use encryption (ie, save a flag in the sessionstore). * From 8f98504183ec9e6ef65c1f10b167505972d8f45e Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 29 Aug 2018 18:11:53 +0100 Subject: [PATCH 186/472] jsdoc --- src/client.js | 4 ++-- src/crypto/index.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client.js b/src/client.js index d345d9a44..9a538b77d 100644 --- a/src/client.js +++ b/src/client.js @@ -706,7 +706,7 @@ MatrixClient.prototype.isRoomEncrypted = function(roomId) { * Forces the current outbound group session to be discarded such * that another one will be created next time an event is sent. * - * @param roomId The ID of the room to discard the session for + * @param {string} roomId The ID of the room to discard the session for * * This should not normally be necessary. */ @@ -714,7 +714,7 @@ MatrixClient.prototype.forceDiscardSession = function(roomId) { if (!this._crypto) { throw new Error("End-to-End encryption disabled"); } - return this._crypto.forceDiscardSession(roomId); + this._crypto.forceDiscardSession(roomId); }; /** diff --git a/src/crypto/index.js b/src/crypto/index.js index a8f4207c1..f00477d4b 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -625,7 +625,7 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) { * Forces the current outbound group session to be discarded such * that another one will be created next time an event is sent. * - * @param roomId The ID of the room to discard the session for + * @param {string} roomId The ID of the room to discard the session for * * This should not normally be necessary. */ From ae95a496182174bb1bd76f1fe848a5ef3fd9418b Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 29 Aug 2018 20:20:52 +0100 Subject: [PATCH 187/472] spell out m.new_devices no longer exist --- src/crypto/algorithms/megolm.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index a4eb32ab0..22d98078e 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -488,6 +488,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, }; @@ -550,12 +552,9 @@ MegolmEncryption.prototype._getDevicesInRoom = async 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 + // 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) { From 7ee93cb910068177a081456b62905d6efed09477 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 30 Aug 2018 10:58:31 +0200 Subject: [PATCH 188/472] 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 --- src/models/room.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 467a7159b..89f77248f 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -279,8 +279,7 @@ Room.prototype.getDMInviter = function() { } if (this._syncedMembership === "invite") { // fall back to summary information - const memberCount = this.currentState.getJoinedMemberCount() + - this.currentState.getInvitedMemberCount(); + const memberCount = this.getInvitedAndJoinedMemberCount(); if (memberCount == 2 && this._summaryHeroes.length) { return this._summaryHeroes[0]; } @@ -490,8 +489,13 @@ Room.prototype.setSummary = function(summary) { if (Number.isInteger(invitedCount)) { this.currentState.setInvitedMemberCount(invitedCount); } - if (heroes) { - this._summaryHeroes = heroes; + 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; + }); } }; @@ -649,6 +653,14 @@ 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. From 267b831bc47b7bd479cf2e3a0969d9d5e8d87d75 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 30 Aug 2018 11:05:05 +0200 Subject: [PATCH 189/472] calculate fallback avatar for rooms with <= members but not DM --- src/models/room.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/models/room.js b/src/models/room.js index 89f77248f..dd80e7d24 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -286,6 +286,24 @@ Room.prototype.getDMInviter = function() { } }; + +Room.prototype.getAvatarFallbackMember = function() { + const memberCount = this.getInvitedAndJoinedMemberCount(); + if (memberCount <= 2) { + if (this._summaryHeroes.length) { + return this._summaryHeroes[0]; + } + const members = this.currentState.getMembers(); + // could be different than memberCount + // as this includes left members + if (members.length <= 2) { + return members.find((m) => { + return m.userId !== this.myUserId + }); + } + } +} + /** * Sets the membership this room was received as during sync * @param {string} membership join | leave | invite From c335a6b3decc0c7c680c212b2d25a4b9efa57007 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 30 Aug 2018 11:05:31 +0200 Subject: [PATCH 190/472] guess DM user id, used to patch up incorrect m.direct account data --- src/models/room.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/models/room.js b/src/models/room.js index dd80e7d24..b94d308fc 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -287,6 +287,43 @@ Room.prototype.getDMInviter = function() { }; + +/** + * Assuming this room is a DM room, tries to guess with which user. + * @return {string} user id of the other member, if found, otherwise undefined. + * Will never return the syncing users user id. + */ +Room.prototype.guessDMUserId = function() { + const me = this.getMember(this.myUserId); + if (me) { + const inviterId = me.getDMInviter(); + if (inviterId) { + return inviterId; + } + } + const fallbackMember = this.getAvatarFallbackMember(); + if (fallbackMember) { + return fallbackMember.userId; + } + // now we're getting into sketchy territory, + // but we're assuming this room is marked as a DM + // so we're going to make a wild-ish guess with whom + if (this._summaryHeroes.length) { + return this._summaryHeroes[0]; + } + const anyMember = members.filter((m) => m.userId !== this.myUserId); + if (anyMember) { + return anyMember; + } + const createEvent = this.currentState.getStateEvents("m.room.create", ""); + if (createEvent) { + const sender = createEvent.getSender(); + if (sender !== this.myUserId) { + return sender; + } + } +}; + Room.prototype.getAvatarFallbackMember = function() { const memberCount = this.getInvitedAndJoinedMemberCount(); if (memberCount <= 2) { From 3659e86d57129bf302c62db7bd7e105d48a1300f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 30 Aug 2018 11:41:35 +0200 Subject: [PATCH 191/472] fix lint, actually get members as well --- src/models/room.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index b94d308fc..e752bf425 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -286,8 +286,6 @@ Room.prototype.getDMInviter = function() { } }; - - /** * Assuming this room is a DM room, tries to guess with which user. * @return {string} user id of the other member, if found, otherwise undefined. @@ -311,6 +309,7 @@ Room.prototype.guessDMUserId = function() { if (this._summaryHeroes.length) { return this._summaryHeroes[0]; } + const members = this.currentState.getMembers(); const anyMember = members.filter((m) => m.userId !== this.myUserId); if (anyMember) { return anyMember; @@ -335,11 +334,11 @@ Room.prototype.getAvatarFallbackMember = function() { // as this includes left members if (members.length <= 2) { return members.find((m) => { - return m.userId !== this.myUserId + return m.userId !== this.myUserId; }); } } -} +}; /** * Sets the membership this room was received as during sync From 3de3ea38b9a43d8505fb6d60256685fe637c6167 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 30 Aug 2018 11:47:17 +0200 Subject: [PATCH 192/472] check heroes is present --- src/models/room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room.js b/src/models/room.js index e752bf425..6d7e7c400 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -326,7 +326,7 @@ Room.prototype.guessDMUserId = function() { Room.prototype.getAvatarFallbackMember = function() { const memberCount = this.getInvitedAndJoinedMemberCount(); if (memberCount <= 2) { - if (this._summaryHeroes.length) { + if (this._summaryHeroes && this._summaryHeroes.length) { return this._summaryHeroes[0]; } const members = this.currentState.getMembers(); From 7f5a2974ce3e8926a91c917aa7f8749caf5525c1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 30 Aug 2018 12:37:13 +0200 Subject: [PATCH 193/472] allow self chats --- src/models/room.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 6d7e7c400..7a8992531 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -288,8 +288,7 @@ Room.prototype.getDMInviter = function() { /** * Assuming this room is a DM room, tries to guess with which user. - * @return {string} user id of the other member, if found, otherwise undefined. - * Will never return the syncing users user id. + * @return {string} user id of the other member (could be syncing user) */ Room.prototype.guessDMUserId = function() { const me = this.getMember(this.myUserId); @@ -321,6 +320,10 @@ Room.prototype.guessDMUserId = function() { return sender; } } + // 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() { From 60c01d7869762b9286c451cfd6d2979f78c07d98 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 30 Aug 2018 12:03:53 +0100 Subject: [PATCH 194/472] Revert b0b0291 and a6de395 To make tests pass again --- spec/unit/room.spec.js | 4 ++-- src/models/room.js | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 5f5f87a3f..b98559ecf 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -667,7 +667,7 @@ describe("Room", function() { const roomName = "flibble"; const event = addMember(userA, "invite"); - event.event.unsigned.invite_room_state = [ + event.event.invite_room_state = [ { type: "m.room.name", state_key: "", @@ -686,7 +686,7 @@ describe("Room", function() { const roomName = "flibble"; setRoomName(roomName); const roomNameToIgnore = "ignoreme"; - event.event.unsigned.invite_room_state = [ + event.event.invite_room_state = [ { type: "m.room.name", state_key: "", diff --git a/src/models/room.js b/src/models/room.js index 7a8992531..644298a91 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1215,10 +1215,7 @@ Room.prototype.recalculate = function() { "m.room.member", this.myUserId, ); if (membershipEvent && membershipEvent.getContent().membership === "invite") { - const strippedStateEvents = ( - membershipEvent.event.unsigned ? - membershipEvent.event.unsigned.invite_room_state : - []) || []; + const strippedStateEvents = membershipEvent.event.invite_room_state || []; utils.forEach(strippedStateEvents, function(strippedEvent) { const existingEvent = self.currentState.getStateEvents( strippedEvent.type, strippedEvent.state_key, From 3e88593a8157ae31d1f2227c33cbb5b12d32ff24 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 30 Aug 2018 14:16:14 +0100 Subject: [PATCH 195/472] Prepare changelog for v0.10.9-rc.1 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7a809834..69c6a2591 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +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) + BREAKING CHANGE --------------- From 938772b86a10ac050e52c2d6480c5ffb7f121278 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 30 Aug 2018 14:16:14 +0100 Subject: [PATCH 196/472] v0.10.9-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 13344acaa..c4e2a1ba3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.10.8", + "version": "0.10.9-rc.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From d72a70396a7d2d1f0a9572d7b2677e279b389da4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 28 Aug 2018 14:54:54 +0200 Subject: [PATCH 197/472] 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. --- src/sync.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/sync.js b/src/sync.js index 4aa2d2482..6bf2e3246 100644 --- a/src/sync.js +++ b/src/sync.js @@ -780,6 +780,13 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) { // instead, so that clients can observe this state // if they wish. this._startKeepAlives().then(() => { + if (this.getSyncState() == 'ERROR') { + this._updateSyncState("PREPARED", { + oldSyncToken: null, + nextSyncToken: null, + catchingUp: true, + }); + } this._sync(syncOptions); }); From d837ae64acfcfbdf2ca6683938533086f0966d65 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 30 Aug 2018 15:23:14 +0200 Subject: [PATCH 198/472] triple = --- src/sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sync.js b/src/sync.js index 6bf2e3246..3b8d031e2 100644 --- a/src/sync.js +++ b/src/sync.js @@ -780,7 +780,7 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) { // instead, so that clients can observe this state // if they wish. this._startKeepAlives().then(() => { - if (this.getSyncState() == 'ERROR') { + if (this.getSyncState() === 'ERROR') { this._updateSyncState("PREPARED", { oldSyncToken: null, nextSyncToken: null, From 0d23d047fccbbf6ec1c780ad5268640082151802 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 30 Aug 2018 15:37:00 +0200 Subject: [PATCH 199/472] use CATCHUP state after ERROR before going back to SYNCING --- src/client.js | 16 +++++++++++----- src/sync.js | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/client.js b/src/client.js index 1f3a48130..3e076480c 100644 --- a/src/client.js +++ b/src/client.js @@ -3504,6 +3504,12 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * a state of SYNCING. This is the equivalent of "syncComplete" in the * previous API. * + *
  • CATCHUP: The client has detected the connection to the server might be + * available again and will now try to do a sync again. As this sync might take + * a long time (depending how long ago was last synced, and general server + * performance) the client is put in this mode so the UI can reflect trying + * to catch up with the server after losing connection.
  • + * *
  • SYNCING : The client is currently polling for new events from the server. * This will be called after processing latest events from a sync.
  • * @@ -3527,11 +3533,11 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * +---->STOPPED * | * +----->PREPARED -------> SYNCING <--+ - * | ^ | ^ | - * | | | | | - * | | V | | - * null ------+ | +--------RECONNECTING | - * | | V | + * | ^ | ^ | + * | CATCHUP ----------+ | | | + * | ^ V | | + * null ------+ | +------- RECONNECTING | + * | V V | * +------->ERROR ---------------------+ * * NB: 'null' will never be emitted by this event. diff --git a/src/sync.js b/src/sync.js index 3b8d031e2..1a4566db2 100644 --- a/src/sync.js +++ b/src/sync.js @@ -781,7 +781,7 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) { // if they wish. this._startKeepAlives().then(() => { if (this.getSyncState() === 'ERROR') { - this._updateSyncState("PREPARED", { + this._updateSyncState("CATCHUP", { oldSyncToken: null, nextSyncToken: null, catchingUp: true, From 1239485b300b71aacb479f59ebf0c58cb4b23af0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 30 Aug 2018 15:42:15 +0200 Subject: [PATCH 200/472] fix test --- spec/unit/matrix-client.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/unit/matrix-client.spec.js b/spec/unit/matrix-client.spec.js index 8ef22f6b0..2fc5d2cf0 100644 --- a/spec/unit/matrix-client.spec.js +++ b/spec/unit/matrix-client.spec.js @@ -380,7 +380,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 +403,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(); }); From 1bb608cdb65e096120ce3e655f52b69e0c46b094 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 30 Aug 2018 17:29:16 +0200 Subject: [PATCH 201/472] remove filter for LL members so invite members are also sent --- src/models/room.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/models/room.js b/src/models/room.js index 644298a91..b0110fec6 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -354,7 +354,6 @@ Room.prototype.setSyncedMembership = function(membership) { Room.prototype._loadMembersFromServer = async function() { const lastSyncToken = this._client.store.getSyncToken(); const queryString = utils.encodeParams({ - membership: "join", not_membership: "leave", at: lastSyncToken, }); From 8e160dda8e2382eee419f2b4ecb860394ffadd4d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 31 Aug 2018 11:02:33 +0200 Subject: [PATCH 202/472] make sure getAvatarFallbackMember always returns a member --- src/models/room.js | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 644298a91..7a26ac3f3 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -328,17 +328,28 @@ Room.prototype.guessDMUserId = function() { Room.prototype.getAvatarFallbackMember = function() { const memberCount = this.getInvitedAndJoinedMemberCount(); - if (memberCount <= 2) { - if (this._summaryHeroes && this._summaryHeroes.length) { - return this._summaryHeroes[0]; + 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) { - return members.find((m) => { - return m.userId !== this.myUserId; - }); + } + 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; } } }; From 414b153d285614f3784d4eba10c8941f4fe76e6e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 31 Aug 2018 11:05:06 +0200 Subject: [PATCH 203/472] also fallback to getting avatar from user --- src/models/room.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/models/room.js b/src/models/room.js index 7a26ac3f3..705e86bfc 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -22,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"); @@ -352,6 +353,19 @@ Room.prototype.getAvatarFallbackMember = function() { 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; + } + } }; /** From 5042eb87e7df88075aa36dbcc509408603c63b94 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 31 Aug 2018 13:40:36 +0100 Subject: [PATCH 204/472] Prepare changelog for v0.10.9-rc.2 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69c6a2591..b6265c720 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +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) From 7582c28c1aeba7ec895a3438bc7a938faf425aa6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 31 Aug 2018 13:40:37 +0100 Subject: [PATCH 205/472] v0.10.9-rc.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c4e2a1ba3..0698caae2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.10.9-rc.1", + "version": "0.10.9-rc.2", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From f8ea1702f8933324d0db21ddc81f026da41fe0b8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 31 Aug 2018 14:40:56 +0200 Subject: [PATCH 206/472] store support for removing out of band members for a room --- src/store/indexeddb-local-backend.js | 76 ++++++++++++++++++++++++--- src/store/indexeddb-remote-backend.js | 4 ++ src/store/indexeddb-store-worker.js | 3 ++ src/store/indexeddb.js | 4 ++ 4 files changed, 79 insertions(+), 8 deletions(-) diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index b24bcbdbe..4df9dc0af 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -71,7 +71,7 @@ function selectQuery(store, keyRange, resultMapper) { }); } -function promiseifyTxn(txn) { +function txnAsPromise(txn) { return new Promise((resolve, reject) => { txn.oncomplete = function(event) { resolve(event); @@ -82,7 +82,7 @@ function promiseifyTxn(txn) { }); } -function promiseifyRequest(req) { +function reqAsEventPromise(req) { return new Promise((resolve, reject) => { req.onsuccess = function(event) { resolve(event); @@ -93,6 +93,17 @@ function promiseifyRequest(req) { }); } +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 * @@ -159,7 +170,7 @@ LocalIndexedDBStoreBackend.prototype = { console.log( `LocalIndexedDBStoreBackend.connect: awaiting connection...`, ); - return promiseifyRequest(req).then((ev) => { + return reqAsEventPromise(req).then((ev) => { console.log( `LocalIndexedDBStoreBackend.connect: connected`, ); @@ -265,7 +276,7 @@ LocalIndexedDBStoreBackend.prototype = { const tx = this.db.transaction(["oob_membership_events"], "readwrite"); const store = tx.objectStore("oob_membership_events"); const eventPuts = membershipEvents.map((e) => { - const putPromise = promiseifyRequest(store.put(e)); + const putPromise = reqAsEventPromise(store.put(e)); // ignoring the result makes sure we discard the IDB success event // ASAP, and not create a potentially big array containing them // unneccesarily later on by calling Promise.all. @@ -281,7 +292,7 @@ LocalIndexedDBStoreBackend.prototype = { oob_written: true, state_key: 0, }; - const markerPut = promiseifyRequest(store.put(markerObject)); + const markerPut = reqAsEventPromise(store.put(markerObject)); const allPuts = eventPuts.concat(markerPut); // ignore the empty array Promise.all creates // as this method should just resolve @@ -292,6 +303,55 @@ LocalIndexedDBStoreBackend.prototype = { }); }, + 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 indexCount = (await reqAsPromise(roomIndex.count(roomRange))).result; + + 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], + ); + const count = + (await reqAsPromise(writeStore.count(membersKeyRange))).result; + + // Leaving this for now to make sure + if (count !== indexCount) { + console.error(`not deleting all members, ` + + `oob_membership_events and its index room ` + + `dont seem to have the same key order`); + } + + console.log(`LL: Deleting ${count} users + marker 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. @@ -389,7 +449,7 @@ LocalIndexedDBStoreBackend.prototype = { roomsData: roomsData, groupsData: groupsData, }); // put == UPSERT - return promiseifyTxn(txn); + return txnAsPromise(txn); }); }, @@ -406,7 +466,7 @@ LocalIndexedDBStoreBackend.prototype = { for (let i = 0; i < accountData.length; i++) { store.put(accountData[i]); // put == UPSERT } - return promiseifyTxn(txn); + return txnAsPromise(txn); }); }, @@ -428,7 +488,7 @@ LocalIndexedDBStoreBackend.prototype = { event: tuple[1], }); // put == UPSERT } - return promiseifyTxn(txn); + return txnAsPromise(txn); }); }, diff --git a/src/store/indexeddb-remote-backend.js b/src/store/indexeddb-remote-backend.js index 7a58ded1b..85f07f86b 100644 --- a/src/store/indexeddb-remote-backend.js +++ b/src/store/indexeddb-remote-backend.js @@ -110,6 +110,10 @@ RemoteIndexedDBStoreBackend.prototype = { return this._doCmd('setOutOfBandMembers', [roomId, membershipEvents]); }, + clearOutOfBandMembers: function(roomId) { + return this._doCmd('clearOutOfBandMembers', [roomId]); + }, + /** * Load all user presence events from the database. This is not cached. * @return {Promise} A list of presence events in their raw form. diff --git a/src/store/indexeddb-store-worker.js b/src/store/indexeddb-store-worker.js index 021156afc..adfc3535b 100644 --- a/src/store/indexeddb-store-worker.js +++ b/src/store/indexeddb-store-worker.js @@ -95,6 +95,9 @@ class IndexedDBStoreWorker { 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; diff --git a/src/store/indexeddb.js b/src/store/indexeddb.js index 5e027537e..0de57c89a 100644 --- a/src/store/indexeddb.js +++ b/src/store/indexeddb.js @@ -242,4 +242,8 @@ IndexedDBStore.prototype.setOutOfBandMembers = function(roomId, membershipEvents return this.backend.setOutOfBandMembers(roomId, membershipEvents); }; +IndexedDBStore.prototype.clearOutOfBandMembers = function(roomId) { + return this.backend.clearOutOfBandMembers(roomId); +}; + module.exports.IndexedDBStore = IndexedDBStore; From 7258fe4e5c16ed889296dbe00d0d2694fdc4b98c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 31 Aug 2018 14:41:25 +0200 Subject: [PATCH 207/472] clear out of band members in store when leaving room --- src/models/room.js | 23 +++++++++++++++++++++++ src/store/stub.js | 4 ++++ src/sync.js | 1 + 3 files changed, 28 insertions(+) diff --git a/src/models/room.js b/src/models/room.js index 705e86bfc..2d253d64e 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -457,6 +457,29 @@ Room.prototype.loadMembersIfNeeded = function() { 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(); + this._membersPromise = null; + await this._client.store.clearOutOfBandMembers(this.roomId); + } +}; + +/** + * called when sync receives this room in the leave section + * to do cleanup after leaving a room. Possibly called multiple times. + */ +Room.prototype.onLeft = 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. * diff --git a/src/store/stub.js b/src/store/stub.js index 9c628b287..d0c2cabc5 100644 --- a/src/store/stub.js +++ b/src/store/stub.js @@ -272,6 +272,10 @@ StubStore.prototype = { setOutOfBandMembers: function() { return Promise.resolve(); }, + + clearOutOfBandMembers: function() { + return Promise.resolve(); + }, }; /** Stub Store class. */ diff --git a/src/sync.js b/src/sync.js index 4aa2d2482..b500ff316 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1106,6 +1106,7 @@ SyncApi.prototype._processSyncResponse = async function( leaveRooms.forEach(function(leaveObj) { const room = leaveObj.room; room.setSyncedMembership("leave"); + room.onLeft(); const stateEvents = self._mapSyncEventsFormat(leaveObj.state, room); From 9b1926f902d751172d505eec5946e872b4288d2c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 31 Aug 2018 16:11:37 +0200 Subject: [PATCH 208/472] also clear out lazy loaded members from storage --- src/models/room-state.js | 16 ++++++++++++++++ src/models/room.js | 1 + 2 files changed, 17 insertions(+) diff --git a/src/models/room-state.js b/src/models/room-state.js index fb049daa9..bb8e93fb9 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -443,6 +443,22 @@ RoomState.prototype.markOutOfBandMembersFailed = function() { 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 diff --git a/src/models/room.js b/src/models/room.js index 2d253d64e..552806238 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -465,6 +465,7 @@ Room.prototype.clearLoadedMembersIfNeeded = async function() { await this.loadMembersIfNeeded(); this._membersPromise = null; await this._client.store.clearOutOfBandMembers(this.roomId); + this.currentState.clearOutOfBandMembers(); } }; From f30136dba3978ec5adaadafa0435373b0683811b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 31 Aug 2018 16:12:54 +0200 Subject: [PATCH 209/472] only clear promise at the end to avoid race between load and clear members --- src/models/room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room.js b/src/models/room.js index 552806238..19f4a7df6 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -463,9 +463,9 @@ Room.prototype.loadMembersIfNeeded = function() { Room.prototype.clearLoadedMembersIfNeeded = async function() { if (this._opts.lazyLoadMembers && this._membersPromise) { await this.loadMembersIfNeeded(); - this._membersPromise = null; await this._client.store.clearOutOfBandMembers(this.roomId); this.currentState.clearOutOfBandMembers(); + this._membersPromise = null; } }; From ebc162e3d88ded04c6ed2ae206ea76132917c9b3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 31 Aug 2018 16:13:34 +0200 Subject: [PATCH 210/472] 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. --- src/sync.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sync.js b/src/sync.js index b500ff316..dda0984e7 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1106,7 +1106,6 @@ SyncApi.prototype._processSyncResponse = async function( leaveRooms.forEach(function(leaveObj) { const room = leaveObj.room; room.setSyncedMembership("leave"); - room.onLeft(); const stateEvents = self._mapSyncEventsFormat(leaveObj.state, room); @@ -1135,6 +1134,8 @@ SyncApi.prototype._processSyncResponse = async function( accountDataEvents.forEach(function(e) { client.emit("event", e); }); + + room.onLeft(); }); // update the notification timeline, if appropriate. From 3bed5969bfa8df7a22abb58e1868c5ed39d50adb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 3 Sep 2018 10:27:00 +0200 Subject: [PATCH 211/472] remove count logging, approach confirmed to work and be according to idb spec --- src/store/indexeddb-local-backend.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index 4df9dc0af..4bcbbe6e2 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -317,8 +317,6 @@ LocalIndexedDBStoreBackend.prototype = { const roomIndex = store.index("room"); const roomRange = IDBKeyRange.only(roomId); - const indexCount = (await reqAsPromise(roomIndex.count(roomRange))).result; - const minStateKeyProm = reqAsCursorPromise( roomIndex.openKeyCursor(roomRange, "next"), ).then((cursor) => cursor && cursor.primaryKey[1]); @@ -336,17 +334,8 @@ LocalIndexedDBStoreBackend.prototype = { [roomId, minStateKey], [roomId, maxStateKey], ); - const count = - (await reqAsPromise(writeStore.count(membersKeyRange))).result; - // Leaving this for now to make sure - if (count !== indexCount) { - console.error(`not deleting all members, ` + - `oob_membership_events and its index room ` + - `dont seem to have the same key order`); - } - - console.log(`LL: Deleting ${count} users + marker for ` + + console.log(`LL: Deleting all users + marker in storage for ` + `room ${roomId}, with key range:`, [roomId, minStateKey], [roomId, maxStateKey]); await reqAsPromise(writeStore.delete(membersKeyRange)); From f84684982fea3f45aa0afb6b9e67d179f24800b2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 3 Sep 2018 11:14:23 +0200 Subject: [PATCH 212/472] add method to query LL state in client --- src/client.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/client.js b/src/client.js index 9a538b77d..7abb00d61 100644 --- a/src/client.js +++ b/src/client.js @@ -3174,6 +3174,14 @@ MatrixClient.prototype.doesServerSupportLazyLoading = async function() { return this._serverSupportsLazyLoading; }; +/* + * Get if lazy loading members is being used. + * @return {boolean} Whether or not members are lazy loaded by this client + */ +MatrixClient.prototype.hasLazyLoadMembersEnabled = function() { + return !!this._clientOpts.lazyLoadMembers; +}; + /* * Set a function which is called when /sync returns a 'limited' response. * It is called with a room ID and returns a boolean. It should return 'true' if the SDK From 3363cc4f1d97a7556373db7bab321f4581d81e66 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 3 Sep 2018 15:02:38 +0200 Subject: [PATCH 213/472] shallow-clone the filter, so the timeline filter doesnt get written into it later on --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 9a538b77d..e2be7ac8d 100644 --- a/src/client.js +++ b/src/client.js @@ -2145,7 +2145,7 @@ function(roomId, fromToken, limit, dir, timelineFilter = undefined) { let filter = null; if (this._clientOpts.lazyLoadMembers) { - filter = LAZY_LOADING_MESSAGES_FILTER; + filter = Object.assign({}, LAZY_LOADING_MESSAGES_FILTER); } if (timelineFilter) { // XXX: it's horrific that /messages' filter parameter doesn't match From b1801fc953dbc0d8df29c40b9ff6b407e73e75ff Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 3 Sep 2018 14:09:24 +0100 Subject: [PATCH 214/472] Prepare changelog for v0.10.9 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6265c720..676dbcb26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +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) From 0b6632123b9b4948c2dcb48e6b0dc3fc21c5679d Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 3 Sep 2018 14:09:24 +0100 Subject: [PATCH 215/472] v0.10.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0698caae2..a61085900 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.10.9-rc.2", + "version": "0.10.9", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From 2391ce198d45a1ac9de99935b1daf49d8d0d3ff7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 4 Sep 2018 12:59:37 +0200 Subject: [PATCH 216/472] this method returns userId, not member --- src/models/room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room.js b/src/models/room.js index ce7054493..e29b924e9 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -312,7 +312,6 @@ Room.prototype.guessDMUserId = function() { const members = this.currentState.getMembers(); const anyMember = members.filter((m) => m.userId !== this.myUserId); if (anyMember) { - return anyMember; } const createEvent = this.currentState.getStateEvents("m.room.create", ""); if (createEvent) { @@ -320,6 +319,7 @@ Room.prototype.guessDMUserId = function() { if (sender !== this.myUserId) { return sender; } + 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 From 632e4aa120d2f1433bec3ac6f30e4019dc25611b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 4 Sep 2018 13:00:00 +0200 Subject: [PATCH 217/472] pick the first member, dont need an array --- src/models/room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room.js b/src/models/room.js index e29b924e9..2876b043a 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -310,7 +310,7 @@ Room.prototype.guessDMUserId = function() { return this._summaryHeroes[0]; } const members = this.currentState.getMembers(); - const anyMember = members.filter((m) => m.userId !== this.myUserId); + const anyMember = members.find((m) => m.userId !== this.myUserId); if (anyMember) { } const createEvent = this.currentState.getStateEvents("m.room.create", ""); From 3e32bc0d5d8b6af54d6e0ead67a661db5e6577a9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 4 Sep 2018 13:00:34 +0200 Subject: [PATCH 218/472] check heroes is not falsy first, this would fail without LL --- src/models/room.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 2876b043a..2d47828e2 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -303,10 +303,11 @@ Room.prototype.guessDMUserId = function() { if (fallbackMember) { return fallbackMember.userId; } - // now we're getting into sketchy territory, - // but we're assuming this room is marked as a DM - // so we're going to make a wild-ish guess with whom - if (this._summaryHeroes.length) { + // 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(); From f0d3d0d74ec019af855715bc72cdc61ef72d1de5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 4 Sep 2018 13:01:15 +0200 Subject: [PATCH 219/472] remove unneeded checks, we should always have enough heroes or members --- src/models/room.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 2d47828e2..c3bdba00c 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -299,10 +299,6 @@ Room.prototype.guessDMUserId = function() { return inviterId; } } - const fallbackMember = this.getAvatarFallbackMember(); - if (fallbackMember) { - return fallbackMember.userId; - } // 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) && @@ -313,13 +309,6 @@ Room.prototype.guessDMUserId = function() { const members = this.currentState.getMembers(); const anyMember = members.find((m) => m.userId !== this.myUserId); if (anyMember) { - } - const createEvent = this.currentState.getStateEvents("m.room.create", ""); - if (createEvent) { - const sender = createEvent.getSender(); - if (sender !== this.myUserId) { - return sender; - } return anyMember.userId; } // it really seems like I'm the only user in the room From a3567f091816cbe8918ef884c1d8c4e15dcb8e5c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 4 Sep 2018 13:01:45 +0200 Subject: [PATCH 220/472] some tests for room.guessDMUserId() --- spec/unit/room.spec.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index b98559ecf..a355f95fa 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1411,4 +1411,27 @@ describe("Room", function() { expect(room.getMyMembership()).toEqual("join"); }); }); + + describe("guessDMUserId", function() { + it("should return first hero id", + async 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", + async 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", + async function() { + const room = new Room(roomId, null, userA); + expect(room.guessDMUserId()).toEqual(userA); + }); + }); }); From f0095611bcbd84612fc1e3dc32aee18237d2457b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 4 Sep 2018 16:38:46 +0200 Subject: [PATCH 221/472] add new CATCHUP state as breaking change --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7a809834..37f4f4584 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ BREAKING CHANGE --------------- * `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. Changes in [0.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (TDB) ================================================================================================== From 0d24c18fed83834b3fed2fc8a131739e2f17ce61 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 4 Sep 2018 16:51:36 +0200 Subject: [PATCH 222/472] add comment to explain fix --- src/client.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client.js b/src/client.js index e2be7ac8d..c8e2b8735 100644 --- a/src/client.js +++ b/src/client.js @@ -2145,6 +2145,8 @@ function(roomId, fromToken, limit, dir, timelineFilter = undefined) { let filter = null; if (this._clientOpts.lazyLoadMembers) { + // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER, + // so the timelineFilter doesn't get written into it below filter = Object.assign({}, LAZY_LOADING_MESSAGES_FILTER); } if (timelineFilter) { From 768c66313f0aae2ffb2156bbeabb99972abf4123 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 4 Sep 2018 18:09:47 +0200 Subject: [PATCH 223/472] remove unneeded async keywords --- spec/unit/room.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index a355f95fa..726fcda89 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1414,13 +1414,13 @@ describe("Room", function() { describe("guessDMUserId", function() { it("should return first hero id", - async function() { + 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", - async function() { + function() { const room = new Room(roomId, null, userA); room.addLiveEvents([utils.mkMembership({ user: userB, mship: "join", @@ -1429,7 +1429,7 @@ describe("Room", function() { expect(room.guessDMUserId()).toEqual(userB); }); it("should return self if only member present", - async function() { + function() { const room = new Room(roomId, null, userA); expect(room.guessDMUserId()).toEqual(userA); }); From 8b00083bca312d2b1f3231cd1f95f31de012d82c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 4 Sep 2018 15:30:16 +0200 Subject: [PATCH 224/472] 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 --- CHANGELOG.md | 27 ++++++++++++++------------- spec/unit/room-state.spec.js | 15 --------------- spec/unit/room.spec.js | 15 ++++++++++++++- src/models/room-state.js | 22 +++++++++++++--------- src/models/room.js | 11 +++++++++++ 5 files changed, 52 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aacf9a8b1..647db7cda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +Changes in [0.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (TDB) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.9...v0.11.0) + + * 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. + 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) @@ -36,19 +50,6 @@ Changes in [0.10.9-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta * Lazy loading of room members [\#691](https://github.com/matrix-org/matrix-js-sdk/pull/691) -BREAKING CHANGE ---------------- - - * `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. - -Changes in [0.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (TDB) -================================================================================================== -[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.6...v0.11.0) - - * 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`. - 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) diff --git a/spec/unit/room-state.spec.js b/spec/unit/room-state.spec.js index 24f940184..6619c64a7 100644 --- a/spec/unit/room-state.spec.js +++ b/spec/unit/room-state.spec.js @@ -447,13 +447,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); @@ -640,14 +633,6 @@ 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); - }); - it("should say any member may send events with no power level event", function() { expect(state.maySendEvent('m.room.message', userA)).toEqual(true); diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 726fcda89..774217c41 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1400,7 +1400,7 @@ describe("Room", function() { describe("getMyMembership", function() { it("should return synced membership if membership isn't available yet", - async function() { + function() { const room = new Room(roomId, null, userA); room.setSyncedMembership("invite"); expect(room.getMyMembership()).toEqual("invite"); @@ -1434,4 +1434,17 @@ describe("Room", function() { 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.setSyncedMembership("invite"); + expect(room.maySendMessage()).toEqual(false); + room.setSyncedMembership("leave"); + expect(room.maySendMessage()).toEqual(false); + room.setSyncedMembership("join"); + expect(room.maySendMessage()).toEqual(true); + }); + }); }); diff --git a/src/models/room-state.js b/src/models/room-state.js index bb8e93fb9..f12d9ba83 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -667,11 +667,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; @@ -679,25 +674,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 (utils.isNumber(power_levels.state_default)) { + if (Number.isFinite(power_levels.state_default)) { state_default = power_levels.state_default; } else { state_default = 50; } - if (utils.isNumber(power_levels.events_default)) { + + 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 (utils.isNumber(events_levels[eventType])) { + if (Number.isFinite(events_levels[eventType])) { required_level = events_levels[eventType]; } - return member.powerLevel >= required_level; + return powerLevel >= required_level; }; /** diff --git a/src/models/room.js b/src/models/room.js index 3e82fa0c0..7dd61cfb9 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1485,6 +1485,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. From 04ad3d7c3ced4ced84cdd67cdc4f17a741e48de8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 5 Sep 2018 18:04:19 +0100 Subject: [PATCH 225/472] Revert "Don't rely on members to query if syncing user can post to room" --- CHANGELOG.md | 27 +++++++++++++-------------- spec/unit/room-state.spec.js | 15 +++++++++++++++ spec/unit/room.spec.js | 15 +-------------- src/models/room-state.js | 22 +++++++++------------- src/models/room.js | 11 ----------- 5 files changed, 38 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 647db7cda..aacf9a8b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,3 @@ -Changes in [0.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (TDB) -================================================================================================== -[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.9...v0.11.0) - - * 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. - 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) @@ -50,6 +36,19 @@ Changes in [0.10.9-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta * Lazy loading of room members [\#691](https://github.com/matrix-org/matrix-js-sdk/pull/691) +BREAKING CHANGE +--------------- + + * `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. + +Changes in [0.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (TDB) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.6...v0.11.0) + + * 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`. + 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) diff --git a/spec/unit/room-state.spec.js b/spec/unit/room-state.spec.js index 6619c64a7..24f940184 100644 --- a/spec/unit/room-state.spec.js +++ b/spec/unit/room-state.spec.js @@ -447,6 +447,13 @@ 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); @@ -633,6 +640,14 @@ 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); + }); + it("should say any member may send events with no power level event", function() { expect(state.maySendEvent('m.room.message', userA)).toEqual(true); diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 774217c41..726fcda89 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1400,7 +1400,7 @@ describe("Room", function() { describe("getMyMembership", function() { it("should return synced membership if membership isn't available yet", - function() { + async function() { const room = new Room(roomId, null, userA); room.setSyncedMembership("invite"); expect(room.getMyMembership()).toEqual("invite"); @@ -1434,17 +1434,4 @@ describe("Room", function() { 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.setSyncedMembership("invite"); - expect(room.maySendMessage()).toEqual(false); - room.setSyncedMembership("leave"); - expect(room.maySendMessage()).toEqual(false); - room.setSyncedMembership("join"); - expect(room.maySendMessage()).toEqual(true); - }); - }); }); diff --git a/src/models/room-state.js b/src/models/room-state.js index f12d9ba83..bb8e93fb9 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -667,6 +667,11 @@ 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; @@ -674,34 +679,25 @@ 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 (Number.isFinite(power_levels.state_default)) { + if (utils.isNumber(power_levels.state_default)) { state_default = power_levels.state_default; } else { state_default = 50; } - - 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)) { + if (utils.isNumber(power_levels.events_default)) { events_default = power_levels.events_default; } } let required_level = state ? state_default : events_default; - if (Number.isFinite(events_levels[eventType])) { + if (utils.isNumber(events_levels[eventType])) { required_level = events_levels[eventType]; } - return powerLevel >= required_level; + return member.powerLevel >= required_level; }; /** diff --git a/src/models/room.js b/src/models/room.js index 7dd61cfb9..3e82fa0c0 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1485,17 +1485,6 @@ 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. From a0639a32c7807963fdfded38398174c38cccb632 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Sep 2018 11:47:40 +0100 Subject: [PATCH 226/472] Revert "Revert "Don't rely on members to query if syncing user can post to room"" --- CHANGELOG.md | 27 ++++++++++++++------------- spec/unit/room-state.spec.js | 15 --------------- spec/unit/room.spec.js | 15 ++++++++++++++- src/models/room-state.js | 22 +++++++++++++--------- src/models/room.js | 11 +++++++++++ 5 files changed, 52 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aacf9a8b1..647db7cda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +Changes in [0.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (TDB) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.9...v0.11.0) + + * 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. + 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) @@ -36,19 +50,6 @@ Changes in [0.10.9-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta * Lazy loading of room members [\#691](https://github.com/matrix-org/matrix-js-sdk/pull/691) -BREAKING CHANGE ---------------- - - * `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. - -Changes in [0.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (TDB) -================================================================================================== -[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.6...v0.11.0) - - * 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`. - 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) diff --git a/spec/unit/room-state.spec.js b/spec/unit/room-state.spec.js index 24f940184..6619c64a7 100644 --- a/spec/unit/room-state.spec.js +++ b/spec/unit/room-state.spec.js @@ -447,13 +447,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); @@ -640,14 +633,6 @@ 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); - }); - it("should say any member may send events with no power level event", function() { expect(state.maySendEvent('m.room.message', userA)).toEqual(true); diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 726fcda89..774217c41 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1400,7 +1400,7 @@ describe("Room", function() { describe("getMyMembership", function() { it("should return synced membership if membership isn't available yet", - async function() { + function() { const room = new Room(roomId, null, userA); room.setSyncedMembership("invite"); expect(room.getMyMembership()).toEqual("invite"); @@ -1434,4 +1434,17 @@ describe("Room", function() { 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.setSyncedMembership("invite"); + expect(room.maySendMessage()).toEqual(false); + room.setSyncedMembership("leave"); + expect(room.maySendMessage()).toEqual(false); + room.setSyncedMembership("join"); + expect(room.maySendMessage()).toEqual(true); + }); + }); }); diff --git a/src/models/room-state.js b/src/models/room-state.js index bb8e93fb9..f12d9ba83 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -667,11 +667,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; @@ -679,25 +674,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 (utils.isNumber(power_levels.state_default)) { + if (Number.isFinite(power_levels.state_default)) { state_default = power_levels.state_default; } else { state_default = 50; } - if (utils.isNumber(power_levels.events_default)) { + + 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 (utils.isNumber(events_levels[eventType])) { + if (Number.isFinite(events_levels[eventType])) { required_level = events_levels[eventType]; } - return member.powerLevel >= required_level; + return powerLevel >= required_level; }; /** diff --git a/src/models/room.js b/src/models/room.js index 3e82fa0c0..7dd61cfb9 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1485,6 +1485,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. From f51630eb0789715217f29fda834301a240345a6b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 6 Sep 2018 18:00:32 +0200 Subject: [PATCH 227/472] dont create a promise for every inserted member but await the transaction instead --- src/store/indexeddb-local-backend.js | 51 ++++++++++++---------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index 4bcbbe6e2..16e80b50b 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -267,40 +267,31 @@ LocalIndexedDBStoreBackend.prototype = { * @param {event[]} membershipEvents the membership events to store * @returns {Promise} when all members have been stored */ - setOutOfBandMembers: function(roomId, membershipEvents) { + setOutOfBandMembers: async function(roomId, membershipEvents) { console.log(`LL: backend about to store ${membershipEvents.length}` + ` members for ${roomId}`); - function ignoreResult() {} // run everything in a promise so anything that throws will reject - return new Promise((resolve) =>{ - const tx = this.db.transaction(["oob_membership_events"], "readwrite"); - const store = tx.objectStore("oob_membership_events"); - const eventPuts = membershipEvents.map((e) => { - const putPromise = reqAsEventPromise(store.put(e)); - // ignoring the result makes sure we discard the IDB success event - // ASAP, and not create a potentially big array containing them - // unneccesarily later on by calling Promise.all. - return putPromise.then(ignoreResult); - }); - // 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, - }; - const markerPut = reqAsEventPromise(store.put(markerObject)); - const allPuts = eventPuts.concat(markerPut); - // ignore the empty array Promise.all creates - // as this method should just resolve - // to undefined on success - resolve(Promise.all(allPuts).then(ignoreResult)); - }).then(() => { - console.log(`LL: backend done storing 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); + // ignore the empty array Promise.all creates + // as this method should just resolve + // to undefined on success + await txnAsPromise(tx); + console.log(`LL: backend done storing for ${roomId}!`); }, clearOutOfBandMembers: async function(roomId) { From 7ae6c147faade7b2aa97a964e10d96e616b6a3fd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 6 Sep 2018 18:02:19 +0200 Subject: [PATCH 228/472] lint doesnt like async --- src/store/indexeddb-local-backend.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index 16e80b50b..274deca66 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -265,7 +265,6 @@ LocalIndexedDBStoreBackend.prototype = { * 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: async function(roomId, membershipEvents) { console.log(`LL: backend about to store ${membershipEvents.length}` + From 1511a27f4cb276529b9315eaa78bb04adcf6589f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 6 Sep 2018 18:09:46 +0200 Subject: [PATCH 229/472] update/remove comments --- src/store/indexeddb-local-backend.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index 274deca66..07d9f8139 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -269,7 +269,6 @@ LocalIndexedDBStoreBackend.prototype = { setOutOfBandMembers: async function(roomId, membershipEvents) { console.log(`LL: backend about to store ${membershipEvents.length}` + ` members for ${roomId}`); - // run everything in a promise so anything that throws will reject const tx = this.db.transaction(["oob_membership_events"], "readwrite"); const store = tx.objectStore("oob_membership_events"); membershipEvents.forEach((e) => { @@ -286,9 +285,6 @@ LocalIndexedDBStoreBackend.prototype = { state_key: 0, }; store.put(markerObject); - // ignore the empty array Promise.all creates - // as this method should just resolve - // to undefined on success await txnAsPromise(tx); console.log(`LL: backend done storing for ${roomId}!`); }, From 6b8dd42547595370ebfb3e333ebe19756eb2a7b9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Sep 2018 19:53:52 +0100 Subject: [PATCH 230/472] Fox docstring for sync data.error It's 'error', not 'err' --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 46e44fa1f..bf1edf30b 100644 --- a/src/client.js +++ b/src/client.js @@ -3612,7 +3612,7 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * * @param {?Object} data Data about this transition. * - * @param {MatrixError} data.err The matrix error if state=ERROR. + * @param {MatrixError} data.error The matrix error if state=ERROR. * * @param {String} data.oldSyncToken The 'since' token passed to /sync. * null for the first successful sync since this client was From ba4dc6c60a6db61d0dd5999a50bfa906073d72f3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 7 Sep 2018 10:44:06 +0200 Subject: [PATCH 231/472] dont wait for LL members to be stored to resolve the members this can easily add up to 100ms / 1000 of members --- src/models/room.js | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 7dd61cfb9..b5ce503cb 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -413,8 +413,21 @@ Room.prototype.loadMembersIfNeeded = function() { // the OOB members this.currentState.markOutOfBandMembersStarted(); - const promise = this._loadMembers().then(({memberEvents, fromServer}) => { - this.currentState.setOutOfBandMembers(memberEvents); + 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()) @@ -431,17 +444,12 @@ Room.prototype.loadMembersIfNeeded = function() { }); } }).catch((err) => { - // allow retries on fail - this._membersPromise = null; - this.currentState.markOutOfBandMembersFailed(); - throw err; + // as this is not awaited anywhere, + // at least show the error in the console + console.error(err); }); - this._membersPromise = promise; - // 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); - } + this._membersPromise = inMemoryUpdate; return this._membersPromise; }; From affdfccd606c4fa856db66e11dd5ff2c038cd2a8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 7 Sep 2018 11:29:44 +0100 Subject: [PATCH 232/472] 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. --- src/sync.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/sync.js b/src/sync.js index 970a162c9..69575a4d0 100644 --- a/src/sync.js +++ b/src/sync.js @@ -779,8 +779,12 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) { // erroneous. We set the state to 'reconnecting' // instead, so that clients can observe this state // if they wish. - this._startKeepAlives().then(() => { - if (this.getSyncState() === 'ERROR') { + 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, @@ -1215,13 +1219,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; } } @@ -1246,8 +1253,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 @@ -1255,7 +1263,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 }); } }); From 9e02049b0551c1e9b068a03b1a91289285dc0e75 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 7 Sep 2018 14:11:27 +0200 Subject: [PATCH 233/472] RoomState.members emitted with wrong argument order for OOB members --- src/models/room-state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room-state.js b/src/models/room-state.js index f12d9ba83..85d8a744f 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -511,7 +511,7 @@ RoomState.prototype._setOutOfBandMember = function(stateEvent) { this._setStateEvent(stateEvent); this._updateMember(member); - this.emit("RoomState.members", {}, stateEvent, member); + this.emit("RoomState.members", stateEvent, this, member); }; /** From 7e8f25bce33c28b99d28b3cbfe8d22866b3c8b5d Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 7 Sep 2018 13:18:45 +0100 Subject: [PATCH 234/472] Include 404 in connectivity success error codes --- src/sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sync.js b/src/sync.js index 69575a4d0..bcb131c59 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1245,7 +1245,7 @@ SyncApi.prototype._pokeKeepAlive = function(connDidFail) { ).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 From c91b67d37093cf5ba6abf7dba9f115b4ac4a3a84 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 7 Sep 2018 14:28:44 +0100 Subject: [PATCH 235/472] Prepare changelog for v0.11.0-rc.1 --- CHANGELOG.md | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 647db7cda..02d60613c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ -Changes in [0.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (TDB) -================================================================================================== -[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.9...v0.11.0) +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`. @@ -12,6 +12,34 @@ BREAKING CHANGES * 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) From 387fd16b403d0145566bc303fe837ef1e7a065cb Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 7 Sep 2018 14:28:45 +0100 Subject: [PATCH 236/472] v0.11.0-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a61085900..7f3fb8675 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.10.9", + "version": "0.11.0-rc.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From c5b62903f3b19e21c22f12e2517562ee47c71d69 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Sep 2018 11:33:50 +0100 Subject: [PATCH 237/472] Prepare changelog for v0.11.0 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02d60613c..8b51feeb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +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) From 33a11397727242b6e7cd2cf271f157dcbbcce3b1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Sep 2018 11:33:51 +0100 Subject: [PATCH 238/472] v0.11.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f3fb8675..6f1ecfc14 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.11.0-rc.1", + "version": "0.11.0", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From 98e448acddd6314071439ddbcfa61e048c5bbd33 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 11 Sep 2018 14:13:35 +0200 Subject: [PATCH 239/472] state events from context response were not wrapped in a MatrixEvent --- src/client.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index bf1edf30b..8f0ca893d 100644 --- a/src/client.js +++ b/src/client.js @@ -2102,7 +2102,8 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) { self.getEventMapper())); timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end; } else { - timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(res.state); + const stateEvents = utils.map(res.state, self.getEventMapper()); + timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(stateEvents); } timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start); From daa7af06050d3a93f4319928f502239c3ff07f22 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 13 Sep 2018 09:52:21 +0200 Subject: [PATCH 240/472] room name should only take canonical alias into account --- src/models/room.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index b5ce503cb..e5d794f0d 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1525,14 +1525,6 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { } let alias = room.getCanonicalAlias(); - - if (!alias) { - const aliases = room.getAliases(); - - if (aliases.length) { - alias = aliases[0]; - } - } if (alias) { return alias; } From b829a39cd2efaf874c4c742a1d0bce11bf0e05bb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 13 Sep 2018 09:59:20 +0200 Subject: [PATCH 241/472] fix tests --- spec/unit/room.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 774217c41..3937613c9 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -863,24 +863,24 @@ describe("Room", function() { expect(name.indexOf(userB)).toNotEqual(-1, name); }); - it("should show the room alias if one exists for private " + + it("should not show the room alias if one exists for private " + "(invite join_rules) rooms if a room name doesn't exist.", function() { const alias = "#room_alias:here"; setJoinRule("invite"); setAliases([alias, "#another:one"]); room.recalculate(); const name = room.name; - expect(name).toEqual(alias); + expect(name).toEqual("Empty room"); }); - it("should show the room alias if one exists for public " + + it("should not show the room alias if one exists for public " + "(public join_rules) rooms if a room name doesn't exist.", function() { const alias = "#room_alias:here"; setJoinRule("public"); setAliases([alias, "#another:one"]); room.recalculate(); const name = room.name; - expect(name).toEqual(alias); + expect(name).toEqual("Empty room"); }); it("should show the room name if one exists for private " + From 907e9fc476ad45ba2d4a527e76408019a872680e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 13 Sep 2018 10:04:31 +0200 Subject: [PATCH 242/472] fix lint --- src/models/room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room.js b/src/models/room.js index e5d794f0d..bdbf1c52d 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1524,7 +1524,7 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { } } - let alias = room.getCanonicalAlias(); + const alias = room.getCanonicalAlias(); if (alias) { return alias; } From 3838fab7889cfcc897414710ef9b75be5c30f44e Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 13 Sep 2018 17:01:05 +0100 Subject: [PATCH 243/472] WIP e2e key backup support Continues from uhoreg's branch --- src/client.js | 68 +++++++++++++++++++++++++--- src/crypto/algorithms/megolm.js | 68 +++++++--------------------- src/crypto/index.js | 80 ++++++++++++++++++++++++++++++++- 3 files changed, 155 insertions(+), 61 deletions(-) diff --git a/src/client.js b/src/client.js index 1dbe4bfe9..78a43f163 100644 --- a/src/client.js +++ b/src/client.js @@ -774,9 +774,27 @@ MatrixClient.prototype.getKeyBackupVersion = function(callback) { } return res; } + }).catch(e => { + if (e.errcode === 'M_NOT_FOUND') { + if (callback) callback(null); + return null; + } else { + throw e; + } }); } +/** + * @returns {bool} true if the client is configured to back up keys to + * the server, otherwise false. + */ +MatrixClient.prototype.getKeyBackupEnabled = function() { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + return Boolean(this._crypto.backupKey); +} + /** * Enable backing up of keys, using data previously returned from * getKeyBackupVersion. @@ -786,6 +804,7 @@ MatrixClient.prototype.enableKeyBackup = function(info) { throw new Error("End-to-end encryption disabled"); } + this._crypto.backupInfo = info; this._crypto.backupKey = new global.Olm.PkEncryption(); this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); } @@ -798,7 +817,8 @@ MatrixClient.prototype.disableKeyBackup = function() { throw new Error("End-to-end encryption disabled"); } - this._crypto.backupKey = undefined; + this._crypto.backupInfo = null; + this._crypto.backupKey = null; } /** @@ -836,11 +856,16 @@ MatrixClient.prototype.createKeyBackupVersion = function(info, callback) { algorithm: info.algorithm, auth_data: info.auth_data, // FIXME: should this be cloned? } - this._crypto._signObject(data.auth_data); - return this._http.authedRequest( - undefined, "POST", "/room_keys/version", undefined, data, - ).then((res) => { - this.enableKeyBackup(info); + return this._crypto._signObject(data.auth_data).then(() => { + return this._http.authedRequest( + undefined, "POST", "/room_keys/version", undefined, data, + ); + }).then((res) => { + this.enableKeyBackup({ + algorithm: info.algorithm, + auth_data: info.auth_data, + version: res.version, + }); if (callback) { callback(null, res); } @@ -848,6 +873,27 @@ MatrixClient.prototype.createKeyBackupVersion = function(info, callback) { }); } +MatrixClient.prototype.deleteKeyBackupVersion = function(version) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + // If we're currently backing up to this backup... stop. + // (We start using it automatically in createKeyBackupVersion + // so this is symmetrical). + if (this._crypto.backupInfo && this._crypto.backupInfo.version === version) { + this.disableKeyBackup(); + } + + const path = utils.encodeUri("/room_keys/version/$version", { + $version: version, + }); + + return this._http.authedRequest( + undefined, "DELETE", path, undefined, undefined, + ); +}; + MatrixClient.prototype._makeKeyBackupPath = function(roomId, sessionId, version) { let path; if (sessionId !== undefined) { @@ -890,6 +936,14 @@ MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data ); }; +MatrixClient.prototype.backupAllGroupSessions = function(version) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + return this._crypto.backupAllGroupSessions(version); +}; + MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, roomId, sessionId, version, callback) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); @@ -924,7 +978,7 @@ MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, roomId, sessi }) }; -MatrixClient.prototype.deleteKeyBackups = function(roomId, sessionId, version, callback) { +MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, version, callback) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index ac9c72f68..af311e16b 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -263,6 +263,14 @@ MegolmEncryption.prototype._prepareNewSession = async function() { key.key, {ed25519: this._olmDevice.deviceEd25519Key}, ); + if (this._crypto.backupInfo) { + // Not strictly necessary to wait for this + await this._crypto.backupGroupSession( + this._roomId, this._olmDevice.deviceCurve25519Key, [], + sessionId, key.key, + ); + } + return new OutboundSessionInfo(sessionId); }; @@ -840,11 +848,13 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { // have another go at decrypting events sent with this session. this._retryDecryption(senderKey, sessionId); }).then(() => { - return this.backupGroupSession( - content.room_id, senderKey, forwardingKeyChain, - content.session_id, content.session_key, keysClaimed, - exportFormat, - ); + if (this._crypto.backupInfo) { + return this._crypto.backupGroupSession( + content.room_id, senderKey, forwardingKeyChain, + content.session_id, content.session_key, keysClaimed, + exportFormat, + ); + } }).catch((e) => { console.error(`Error handling m.room_key_event: ${e}`); }); @@ -967,54 +977,6 @@ MegolmDecryption.prototype.importRoomKey = function(session) { }); }; -MegolmDecryption.prototype.backupGroupSession = async function( - roomId, senderKey, forwardingCurve25519KeyChain, - sessionId, sessionKey, keysClaimed, - exportFormat, -) { - // new session. - const session = new Olm.InboundGroupSession(); - let first_known_index; - try { - if (exportFormat) { - session.import_session(sessionKey); - } else { - session.create(sessionKey); - } - if (sessionId != session.session_id()) { - throw new Error( - "Mismatched group session ID from senderKey: " + - senderKey, - ); - } - - if (!exportFormat) { - sessionKey = session.export_session(); - } - const first_known_index = session.first_known_index(); - - const sessionData = { - algorithm: olmlib.MEGOLM_ALGORITHM, - sender_key: senderKey, - sender_claimed_keys: keysClaimed, - forwardingCurve25519KeyChain: forwardingCurve25519KeyChain, - session_key: sessionKey - }; - const encrypted = this._crypto.backupKey.encrypt(JSON.stringify(sessionData)); - const data = { - first_message_index: first_known_index, - forwarded_count: forwardingCurve25519KeyChain.length, - is_verified: false, // FIXME: how do we determine this? - session_data: encrypted - }; - return this._baseApis.sendKeyBackup(roomId, sessionId, data); - } catch (e) { - return Promise.reject(e); - } finally { - session.free(); - } -} - /** * Have another go at decrypting events after we receive a key * diff --git a/src/crypto/index.js b/src/crypto/index.js index 54bb0d738..fb8f82614 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -75,7 +75,8 @@ function Crypto(baseApis, sessionStore, userId, deviceId, // track whether this device's megolm keys are being backed up incrementally // to the server or not. // XXX: this should probably have a single source of truth from OlmAccount - this.backupKey = null; + this.backupInfo = null; // The info dict from /room_keys/version + this.backupKey = null; // The encryption key object this._olmDevice = new OlmDevice(sessionStore, cryptoStore); this._deviceList = new DeviceList( @@ -848,6 +849,83 @@ Crypto.prototype.importRoomKeys = function(keys) { }, ); }; + +Crypto.prototype._backupPayloadForSession = function( + senderKey, forwardingCurve25519KeyChain, + sessionId, sessionKey, keysClaimed, + exportFormat, +) { + // new session. + const session = new Olm.InboundGroupSession(); + let first_known_index; + try { + if (exportFormat) { + session.import_session(sessionKey); + } else { + session.create(sessionKey); + } + if (sessionId != session.session_id()) { + throw new Error( + "Mismatched group session ID from senderKey: " + + senderKey, + ); + } + + if (!exportFormat) { + sessionKey = session.export_session(); + } + const first_known_index = session.first_known_index(); + + const sessionData = { + algorithm: olmlib.MEGOLM_ALGORITHM, + sender_key: senderKey, + sender_claimed_keys: keysClaimed, + session_key: sessionKey, + forwarding_curve25519_key_chain: forwardingCurve25519KeyChain, + }; + const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); + return { + first_message_index: first_known_index, + forwarded_count: forwardingCurve25519KeyChain.length, + is_verified: false, // FIXME: how do we determine this? + session_data: encrypted, + }; + } finally { + session.free(); + } +}; + +Crypto.prototype.backupGroupSession = function( + roomId, senderKey, forwardingCurve25519KeyChain, + sessionId, sessionKey, keysClaimed, + exportFormat, +) { + if (!this.backupInfo) { + throw new Error("Key backups are not enabled"); + } + + const data = this._backupPayloadForSession( + senderKey, forwardingCurve25519KeyChain, + sessionId, sessionKey, keysClaimed, + exportFormat, + ); + return this._baseApis.sendKeyBackup(roomId, sessionId, this.backupInfo.version, data); +}; + +Crypto.prototype.backupAllGroupSessions = async function(version) { + const keys = await this.exportRoomKeys(); + const data = {}; + for (const key of keys) { + if (data[key.room_id] === undefined) data[key.room_id] = {sessions: {}}; + + data[key.room_id]['sessions'][key.session_id] = this._backupPayloadForSession( + key.sender_key, key.forwarding_curve25519_key_chain, + key.session_id, key.session_key, key.sender_claimed_keys, true, + ); + } + return this._baseApis.sendKeyBackup(undefined, undefined, version, {rooms: data}); +}; + /* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307 /** * Encrypt an event according to the configuration of the room. From e78974783416302578e0e90b2c64f3ef408c69c6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 14 Sep 2018 17:06:27 +0100 Subject: [PATCH 244/472] Check sigs on e2e backup & enable it if we can --- src/client.js | 44 ++++++++++-- src/crypto/algorithms/megolm.js | 2 + src/crypto/index.js | 114 ++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 5 deletions(-) diff --git a/src/client.js b/src/client.js index 78a43f163..7997b4c29 100644 --- a/src/client.js +++ b/src/client.js @@ -544,7 +544,15 @@ MatrixClient.prototype.setDeviceVerified = function(userId, deviceId, verified) if (verified === undefined) { verified = true; } - return _setDeviceVerification(this, userId, deviceId, verified, null); + const prom = _setDeviceVerification(this, userId, deviceId, verified, null); + + // if one of the user's own devices is being marked as verified / unverified, + // check the key backup status, since whether or not we use this depends on + // whether it has a signature from a verified device + if (userId == this.credentials.userId) { + this._crypto.checkKeyBackup(); + } + return prom; }; /** @@ -752,10 +760,6 @@ MatrixClient.prototype.importRoomKeys = function(keys) { * Get information about the current key backup. */ MatrixClient.prototype.getKeyBackupVersion = function(callback) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - return this._http.authedRequest( undefined, "GET", "/room_keys/version", ).then((res) => { @@ -784,6 +788,20 @@ MatrixClient.prototype.getKeyBackupVersion = function(callback) { }); } +/** + * @param {object} info key backup info dict from getKeyBackupVersion() + * @return {object} { + * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device + * sigs: [ + * valid: [bool], + * device: [DeviceInfo], + * ] + * } + */ +MatrixClient.prototype.isKeyBackupTrusted = function(info) { + return this._crypto.isKeyBackupTrusted(info); +}; + /** * @returns {bool} true if the client is configured to back up keys to * the server, otherwise false. @@ -807,6 +825,8 @@ MatrixClient.prototype.enableKeyBackup = function(info) { this._crypto.backupInfo = info; this._crypto.backupKey = new global.Olm.PkEncryption(); this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); + + this.emit('keyBackupStatus', true); } /** @@ -819,6 +839,8 @@ MatrixClient.prototype.disableKeyBackup = function() { this._crypto.backupInfo = null; this._crypto.backupKey = null; + + this.emit('keyBackupStatus', false); } /** @@ -3972,6 +3994,18 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; * }); */ +/** + * Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled() + * @event module:client~MatrixClient#"keyBackupStatus" + * @param {bool} enabled true if key backup has been enabled, otherwise false + * @example + * matrixClient.on("keyBackupStatus", function(enabled){ + * if (enabled) { + * [...] + * } + * }); + */ + /** * Fires when we want to suggest to the user that they restore their megolm keys * from backup or by cross-signing the device. diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index af311e16b..f3cdbd17f 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -849,6 +849,8 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { this._retryDecryption(senderKey, sessionId); }).then(() => { if (this._crypto.backupInfo) { + // XXX: No retries on this at all: if this request dies for whatever + // reason, this key will never be uploaded. return this._crypto.backupGroupSession( content.room_id, senderKey, forwardingKeyChain, content.session_id, content.session_key, keysClaimed, diff --git a/src/crypto/index.js b/src/crypto/index.js index fb8f82614..357410b2e 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -77,6 +77,7 @@ function Crypto(baseApis, sessionStore, userId, deviceId, // XXX: this should probably have a single source of truth from OlmAccount this.backupInfo = null; // The info dict from /room_keys/version this.backupKey = null; // The encryption key object + this._checkedForBackup = false; // Have we checked the server for a backup we can use? this._olmDevice = new OlmDevice(sessionStore, cryptoStore); this._deviceList = new DeviceList( @@ -180,6 +181,113 @@ Crypto.prototype.init = async function() { ); this._deviceList.saveIfDirty(); } + + this._checkAndStartKeyBackup(); +}; + +/** + * Check the server for an active key backup and + * if one is present and has a valid signature from + * one of the user's verified devices, start backing up + * to it. + */ +Crypto.prototype._checkAndStartKeyBackup = async function() { + console.log("Checking key backup status..."); + let backupInfo; + try { + backupInfo = await this._baseApis.getKeyBackupVersion(); + } catch (e) { + console.log("Error checking for active key backup", e); + if (Number.isFinite(e.httpStatus) && e.httpStatus / 100 === 4) { + // well that's told us. we won't try again. + this._checkedForBackup = true; + } + return; + } + this._checkedForBackup = true; + + const trustInfo = await this.isKeyBackupTrusted(backupInfo); + + if (trustInfo.usable && !this.backupInfo) { + console.log("Found usable key backup: enabling key backups"); + this._baseApis.enableKeyBackup(backupInfo); + } else if (!trustInfo.usable && this.backupInfo) { + console.log("No usable key backup: disabling key backup"); + this._baseApis.disableKeyBackup(); + } else if (!trustInfo.usable && !this.backupInfo) { + console.log("No usable key backup: not enabling key backup"); + } +}; + +/** + * Forces a re-check of the key backup and enables/disables it + * as appropriate + */ +Crypto.prototype.checkKeyBackup = async function(backupInfo) { + this._checkedForBackup = false; + await this._checkAndStartKeyBackup(); +}; + +/** + * @param {object} backupInfo key backup info dict from /room_keys/version + * @return {object} { + * usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device + * sigs: [ + * valid: [bool], + * device: [DeviceInfo], + * ] + * } + */ +Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { + const ret = { + usable: false, + sigs: [], + }; + + if ( + !backupInfo || + !backupInfo.algorithm || + !backupInfo.auth_data || + !backupInfo.auth_data.public_key || + !backupInfo.auth_data.signatures + ) { + console.log("Key backup is absent or missing required data"); + return ret; + } + + const mySigs = backupInfo.auth_data.signatures[this._userId]; + if (!mySigs || mySigs.length === 0) { + console.log("Ignoring key backup because it lacks any signatures from this user"); + return ret; + } + + for (const keyId of Object.keys(mySigs)) { + const device = this._deviceList.getStoredDevice( + this._userId, keyId.split(':')[1], // XXX: is this how we're supposed to get the device ID? + ); + if (!device) { + console.log("Ignoring signature from unknown key " + keyId); + continue; + } + const sigInfo = { device }; + try { + await olmlib.verifySignature( + this._olmDevice, + backupInfo.auth_data, + this._userId, + device.deviceId, + device.getFingerprint(), + ); + sigInfo.valid = true; + } catch (e) { + console.log("Bad signature from device " + device.deviceId, e); + sigInfo.valid = false; + } + ret.sigs.push(sigInfo); + } + + ret.usable = ret.sigs.some(s => s.valid && s.device.isVerified()); + return ret; }; /** @@ -1233,6 +1341,12 @@ Crypto.prototype._onRoomKeyEvent = function(event) { return; } + if (!this._checkedForBackup) { + // don't bother awaiting on this - the important thing is that we retry if we + // haven't managed to check before + this._checkAndStartKeyBackup(); + } + const alg = this._getRoomDecryptor(content.room_id, content.algorithm); alg.onRoomKeyEvent(event); }; From 073fb73ff36aa7f56e810c8950ebe5b59fa26eb4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 17 Sep 2018 15:59:37 +0100 Subject: [PATCH 245/472] Make multi-room key restore work --- src/client.js | 69 ++++++++++++++++++++++++--------- src/crypto/algorithms/megolm.js | 2 + 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/src/client.js b/src/client.js index 7997b4c29..9d6d33eb8 100644 --- a/src/client.js +++ b/src/client.js @@ -68,6 +68,31 @@ try { console.warn("Unable to load crypto module: crypto will be disabled: " + e); } +function keysFromRecoverySession(sessions, decryptionKey, roomId, keys) { + for (const [sessionId, sessionData] of Object.entries(sessions)) { + try { + const decrypted = keyFromRecoverySession(sessionData, decryptionKey, keys); + decrypted.session_id = sessionId; + decrypted.room_id = roomId; + return decrypted; + } catch (e) { + console.log("Failed to decrypt session from backup"); + } + } +} + +function keyFromRecoverySession(session, decryptionKey, keys) { + try { + keys.push(JSON.parse(decryptionKey.decrypt( + session.session_data.ephemeral, + session.session_data.mac, + session.session_data.ciphertext + ))); + } catch (e) { + console.log("Failed to decrypt key from backup", e); + } +} + /** * Construct a Matrix Client. Only directly construct this if you want to use * custom modules. Normally, {@link createClient} should be used @@ -966,7 +991,7 @@ MatrixClient.prototype.backupAllGroupSessions = function(version) { return this._crypto.backupAllGroupSessions(version); }; -MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, roomId, sessionId, version, callback) { +MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, targetRoomId, targetSessionId, version) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } @@ -975,28 +1000,36 @@ MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, roomId, sessi const decryption = new global.Olm.PkDecryption(); decryption.unpickle("secret_key", decryptionKey); - const path = this._makeKeyBackupPath(roomId, sessionId, version); + let totalKeyCount = 0; + const keys = []; + + const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version); return this._http.authedRequest( undefined, "GET", path.path, path.queryData, ).then((res) => { - const keys = []; - // FIXME: for each room, session, if response has multiple - // decrypt response.data.session_data - const session_data = res.session_data; - const key = JSON.parse(decryption.decrypt( - session_data.ephemeral, - session_data.mac, - session_data.ciphertext - )); - // set room_id and session_id - key.room_id = roomId; - key.session_id = sessionId; - keys.push(key); + if (res.rooms) { + for (const [roomId, roomData] of Object.entries(res.rooms)) { + if (!roomData.sessions) continue; + + totalKeyCount += Object.keys(roomData.sessions).length; + const roomKeys = []; + keysFromRecoverySession(roomData.sessions, decryption, roomId, roomKeys); + for (const k of roomKeys) { + k.room_id = roomId; + keys.push(k); + } + } + } else if (res.sessions) { + totalKeyCount = Object.keys(res.sessions).length; + keys.push(...keysFromRecoverySession(res.sessions, decryption, roomId, keys)); + } else { + totalKeyCount = 1; + keys.push(keyFromRecoverySession(res, decryption, keys)); + } + return this.importRoomKeys(keys); }).then(() => { - if (callback) { - callback(); - } + return {total: totalKeyCount, imported: keys.length}; }) }; diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index f3cdbd17f..1e1de101b 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -851,6 +851,8 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { if (this._crypto.backupInfo) { // XXX: No retries on this at all: if this request dies for whatever // reason, this key will never be uploaded. + // More XXX: If this fails it'll cause the message send to fail, + // and this will happen if the backup is deleted from another client. return this._crypto.backupGroupSession( content.room_id, senderKey, forwardingKeyChain, content.session_id, content.session_key, keysClaimed, From 009430e829c3c5112c4b0d5179425c66f742ab03 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 17 Sep 2018 17:04:29 +0100 Subject: [PATCH 246/472] Add isValidRecoveryKey Add method to check if a given string is a valid recovery key --- src/client.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/client.js b/src/client.js index 9d6d33eb8..1c991c7da 100644 --- a/src/client.js +++ b/src/client.js @@ -991,6 +991,24 @@ MatrixClient.prototype.backupAllGroupSessions = function(version) { return this._crypto.backupAllGroupSessions(version); }; +MatrixClient.prototype.isValidRecoveryKey = function(decryptionKey) { + if (this._crypto === null) { + throw new Error("End-to-end encryption disabled"); + } + + const decryption = new global.Olm.PkDecryption(); + try { + // FIXME: see the FIXME in createKeyBackupVersion + decryption.unpickle("secret_key", decryptionKey); + return true; + } catch (e) { + console.log(e); + return false; + } finally { + decryption.free(); + } +}; + MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, targetRoomId, targetSessionId, version) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); From eb690e14e44b67ebc0e13ab807d427e95a202d49 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Sep 2018 18:20:49 +0200 Subject: [PATCH 247/472] 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. --- src/models/room.js | 19 +++++++++++++------ src/sync.js | 13 +++++++------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index b5ce503cb..818d0e263 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -178,7 +178,7 @@ function Room(roomId, client, myUserId, opts) { // read by megolm; boolean value - null indicates "use global value" this._blacklistUnverifiedDevices = null; - this._syncedMembership = null; + this._selfMembership = null; this._summaryHeroes = null; // awaited by getEncryptionTargetMembers while room members are loading @@ -263,7 +263,7 @@ Room.prototype.getMyMembership = function() { return me.membership; } } - return this._syncedMembership; + return this._selfMembership; }; /** @@ -278,7 +278,7 @@ Room.prototype.getDMInviter = function() { return me.getDMInviter(); } } - if (this._syncedMembership === "invite") { + if (this._selfMembership === "invite") { // fall back to summary information const memberCount = this.getInvitedAndJoinedMemberCount(); if (memberCount == 2 && this._summaryHeroes.length) { @@ -362,8 +362,15 @@ Room.prototype.getAvatarFallbackMember = function() { * Sets the membership this room was received as during sync * @param {string} membership join | leave | invite */ -Room.prototype.setSyncedMembership = function(membership) { - this._syncedMembership = membership; +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() { @@ -470,7 +477,7 @@ Room.prototype.clearLoadedMembersIfNeeded = async function() { * called when sync receives this room in the leave section * to do cleanup after leaving a room. Possibly called multiple times. */ -Room.prototype.onLeft = function() { +Room.prototype._cleanupAfterLeaving = function() { this.clearLoadedMembersIfNeeded().catch((err) => { console.error(`error after clearing loaded members from ` + `room ${this.roomId} after leaving`); diff --git a/src/sync.js b/src/sync.js index bcb131c59..39e82abe4 100644 --- a/src/sync.js +++ b/src/sync.js @@ -123,6 +123,7 @@ SyncApi.prototype.createRoom = function(roomId) { "Room.timelineReset", "Room.localEchoUpdated", "Room.accountData", + "Room.myMembership", ]); this._registerStateListeners(room); return room; @@ -976,9 +977,10 @@ SyncApi.prototype._processSyncResponse = async function( // Handle invites inviteRooms.forEach(function(inviteObj) { const room = inviteObj.room; - room.setSyncedMembership("invite"); const stateEvents = self._mapSyncEventsFormat(inviteObj.invite_state, room); + + room.updateMyMembership("invite"); self._processRoomEvents(room, stateEvents); if (inviteObj.isBrandNewRoom) { room.recalculate(); @@ -993,7 +995,6 @@ SyncApi.prototype._processSyncResponse = async function( // Handle joins await Promise.mapSeries(joinRooms, async function(joinObj) { const room = joinObj.room; - room.setSyncedMembership("join"); const stateEvents = self._mapSyncEventsFormat(joinObj.state, room); const timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room); const ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral); @@ -1009,6 +1010,8 @@ SyncApi.prototype._processSyncResponse = async function( ); } + room.updateMyMembership("join"); + joinObj.timeline = joinObj.timeline || {}; if (joinObj.isBrandNewRoom) { @@ -1116,8 +1119,6 @@ SyncApi.prototype._processSyncResponse = async function( // Handle leaves (e.g. kicked rooms) leaveRooms.forEach(function(leaveObj) { const room = leaveObj.room; - room.setSyncedMembership("leave"); - const stateEvents = self._mapSyncEventsFormat(leaveObj.state, room); const timelineEvents = @@ -1125,6 +1126,8 @@ SyncApi.prototype._processSyncResponse = async function( const accountDataEvents = self._mapSyncEventsFormat(leaveObj.account_data); + room.updateMyMembership("leave"); + self._processRoomEvents(room, stateEvents, timelineEvents); room.addAccountData(accountDataEvents); @@ -1145,8 +1148,6 @@ SyncApi.prototype._processSyncResponse = async function( accountDataEvents.forEach(function(e) { client.emit("event", e); }); - - room.onLeft(); }); // update the notification timeline, if appropriate. From 4630733b5591e1f4a598ed4a929e06af04c5910c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Sep 2018 18:23:48 +0200 Subject: [PATCH 248/472] don't fall back anymore to member, as this is more reliable --- src/models/room.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 818d0e263..c99d87ba5 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -257,12 +257,6 @@ Room.prototype.getLiveTimeline = function() { * @return {string} the membership type (join | leave | invite) for the logged in user */ Room.prototype.getMyMembership = function() { - if (this.myUserId) { - const me = this.getMember(this.myUserId); - if (me) { - return me.membership; - } - } return this._selfMembership; }; From 84e41c2ade6129b17922de2b6d168d33bff913f7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Sep 2018 18:28:07 +0200 Subject: [PATCH 249/472] fix tests --- spec/unit/room.spec.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 774217c41..a7d315575 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1402,13 +1402,8 @@ describe("Room", function() { it("should return synced membership if membership isn't available yet", function() { const room = new Room(roomId, null, userA); - room.setSyncedMembership("invite"); + room.updateMyMembership("invite"); expect(room.getMyMembership()).toEqual("invite"); - room.addLiveEvents([utils.mkMembership({ - user: userA, mship: "join", - room: roomId, event: true, - })]); - expect(room.getMyMembership()).toEqual("join"); }); }); @@ -1439,11 +1434,11 @@ describe("Room", function() { it("should return false if synced membership not join", function() { const room = new Room(roomId, null, userA); - room.setSyncedMembership("invite"); + room.updateMyMembership("invite"); expect(room.maySendMessage()).toEqual(false); - room.setSyncedMembership("leave"); + room.updateMyMembership("leave"); expect(room.maySendMessage()).toEqual(false); - room.setSyncedMembership("join"); + room.updateMyMembership("join"); expect(room.maySendMessage()).toEqual(true); }); }); From fc3a00054f23261c0578c1441d086da28df0a2fe Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Sep 2018 19:33:36 +0200 Subject: [PATCH 250/472] add test for new event --- spec/unit/room.spec.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index a7d315575..4a8ea7e59 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -1405,6 +1405,23 @@ describe("Room", function() { 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() { From f75d188131bb40d30ea2e5d401b6227fb367d437 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 17 Sep 2018 19:25:42 +0100 Subject: [PATCH 251/472] Soe progress on linting --- src/client.js | 80 +++++++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/src/client.js b/src/client.js index 1c991c7da..c6d7e9f9e 100644 --- a/src/client.js +++ b/src/client.js @@ -86,7 +86,7 @@ function keyFromRecoverySession(session, decryptionKey, keys) { keys.push(JSON.parse(decryptionKey.decrypt( session.session_data.ephemeral, session.session_data.mac, - session.session_data.ciphertext + session.session_data.ciphertext, ))); } catch (e) { console.log("Failed to decrypt key from backup", e); @@ -783,35 +783,30 @@ MatrixClient.prototype.importRoomKeys = function(keys) { /** * Get information about the current key backup. + * @returns {Promise} Information object from API or null */ -MatrixClient.prototype.getKeyBackupVersion = function(callback) { +MatrixClient.prototype.getKeyBackupVersion = function() { return this._http.authedRequest( undefined, "GET", "/room_keys/version", ).then((res) => { if (res.algorithm !== olmlib.MEGOLM_BACKUP_ALGORITHM) { const err = "Unknown backup algorithm: " + res.algorithm; - callback(err); return Promise.reject(err); } else if (!(typeof res.auth_data === "object") || !res.auth_data.public_key) { const err = "Invalid backup data returned"; - callback(err); return Promise.reject(err); } else { - if (callback) { - callback(null, res); - } return res; } - }).catch(e => { + }).catch((e) => { if (e.errcode === 'M_NOT_FOUND') { - if (callback) callback(null); return null; } else { throw e; } }); -} +}; /** * @param {object} info key backup info dict from getKeyBackupVersion() @@ -836,11 +831,13 @@ MatrixClient.prototype.getKeyBackupEnabled = function() { throw new Error("End-to-end encryption disabled"); } return Boolean(this._crypto.backupKey); -} +}; /** * Enable backing up of keys, using data previously returned from * getKeyBackupVersion. + * + * @param {object} info Backup information object as returned by getKeyBackupVersion */ MatrixClient.prototype.enableKeyBackup = function(info) { if (this._crypto === null) { @@ -852,7 +849,7 @@ MatrixClient.prototype.enableKeyBackup = function(info) { this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); this.emit('keyBackupStatus', true); -} +}; /** * Disable backing up of keys. @@ -866,35 +863,41 @@ MatrixClient.prototype.disableKeyBackup = function() { this._crypto.backupKey = null; this.emit('keyBackupStatus', false); -} +}; /** * Set up the data required to create a new backup version. The backup version * will not be created and enabled until createKeyBackupVersion is called. + * + * @returns {object} Object that can be passed to createKeyBackupVersion and + * additionally has a 'recovery_key' member with the user-facing recovery key string. */ -MatrixClient.prototype.prepareKeyBackupVersion = function(callback) { +MatrixClient.prototype.prepareKeyBackupVersion = function() { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } const decryption = new global.Olm.PkDecryption(); - const public_key = decryption.generate_key(); + const publicKey = decryption.generate_key(); return { algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, auth_data: { - public_key: public_key, + public_key: publicKey, }, // FIXME: pickle isn't the right thing to use, but we don't have // anything else yet, so use it for now recovery_key: decryption.pickle("secret_key"), }; -} +}; /** * Create a new key backup version and enable it, using the information return * from prepareKeyBackupVersion. + * + * @param {object} info Info object from prepareKeyBackupVersion + * @returns {Promise} Object with 'version' param indicating the version created */ -MatrixClient.prototype.createKeyBackupVersion = function(info, callback) { +MatrixClient.prototype.createKeyBackupVersion = function(info) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } @@ -902,7 +905,7 @@ MatrixClient.prototype.createKeyBackupVersion = function(info, callback) { const data = { algorithm: info.algorithm, auth_data: info.auth_data, // FIXME: should this be cloned? - } + }; return this._crypto._signObject(data.auth_data).then(() => { return this._http.authedRequest( undefined, "POST", "/room_keys/version", undefined, data, @@ -913,12 +916,9 @@ MatrixClient.prototype.createKeyBackupVersion = function(info, callback) { auth_data: info.auth_data, version: res.version, }); - if (callback) { - callback(null, res); - } return res; }); -} +}; MatrixClient.prototype.deleteKeyBackupVersion = function(version) { if (this._crypto === null) { @@ -955,31 +955,30 @@ MatrixClient.prototype._makeKeyBackupPath = function(roomId, sessionId, version) } else { path = "/room_keys/keys"; } - const queryData = version === undefined ? undefined : {version : version}; + const queryData = version === undefined ? undefined : { version: version }; return { path: path, queryData: queryData, - } -} + }; +}; /** * Back up session keys to the homeserver. * @param {string} roomId ID of the room that the keys are for Optional. * @param {string} sessionId ID of the session that the keys are for Optional. * @param {integer} version backup version Optional. - * @param {object} key data - * @param {module:client.callback} callback Optional. + * @param {object} data Object keys to send * @return {module:client.Promise} a promise that will resolve when the keys * are uploaded */ -MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data, callback) { +MatrixClient.prototype.sendKeyBackup = function(roomId, sessionId, version, data) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } const path = this._makeKeyBackupPath(roomId, sessionId, version); return this._http.authedRequest( - callback, "PUT", path.path, path.queryData, data, + undefined, "PUT", path.path, path.queryData, data, ); }; @@ -1009,7 +1008,9 @@ MatrixClient.prototype.isValidRecoveryKey = function(decryptionKey) { } }; -MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, targetRoomId, targetSessionId, version) { +MatrixClient.prototype.restoreKeyBackups = function( + decryptionKey, targetRoomId, targetSessionId, version, +) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } @@ -1039,27 +1040,32 @@ MatrixClient.prototype.restoreKeyBackups = function(decryptionKey, targetRoomId, } } else if (res.sessions) { totalKeyCount = Object.keys(res.sessions).length; - keys.push(...keysFromRecoverySession(res.sessions, decryption, roomId, keys)); + keys.push(...keysFromRecoverySession( + res.sessions, decryption, targetRoomId, keys, + )); } else { totalKeyCount = 1; - keys.push(keyFromRecoverySession(res, decryption, keys)); + const key = keyFromRecoverySession(res, decryption, keys); + key.room_id = targetRoomId; + key.session_id = targetSessionId; + keys.push(key); } return this.importRoomKeys(keys); }).then(() => { return {total: totalKeyCount, imported: keys.length}; - }) + }); }; -MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, version, callback) { +MatrixClient.prototype.deleteKeysFromBackup = function(roomId, sessionId, version) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } const path = this._makeKeyBackupPath(roomId, sessionId, version); return this._http.authedRequest( - callback, "DELETE", path.path, path.queryData, - ) + undefined, "DELETE", path.path, path.queryData, + ); }; // Group ops From 3af9af96ea2ded3f75c4333f5c56f478fc56f815 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 17 Sep 2018 19:31:37 +0100 Subject: [PATCH 252/472] More linting --- src/crypto/index.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 357410b2e..d45797fcb 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -36,6 +36,11 @@ const DeviceList = require('./DeviceList').default; import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; +const Olm = global.Olm; +if (!Olm) { + throw new Error("global.Olm is not defined"); +} + /** * Cryptography bits * @@ -222,6 +227,8 @@ Crypto.prototype._checkAndStartKeyBackup = async function() { /** * Forces a re-check of the key backup and enables/disables it * as appropriate + * + * @param {object} backupInfo Backup info from /room_keys/version endpoint */ Crypto.prototype.checkKeyBackup = async function(backupInfo) { this._checkedForBackup = false; @@ -286,7 +293,7 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) { ret.sigs.push(sigInfo); } - ret.usable = ret.sigs.some(s => s.valid && s.device.isVerified()); + ret.usable = ret.sigs.some((s) => s.valid && s.device.isVerified()); return ret; }; @@ -965,7 +972,6 @@ Crypto.prototype._backupPayloadForSession = function( ) { // new session. const session = new Olm.InboundGroupSession(); - let first_known_index; try { if (exportFormat) { session.import_session(sessionKey); @@ -982,7 +988,7 @@ Crypto.prototype._backupPayloadForSession = function( if (!exportFormat) { sessionKey = session.export_session(); } - const first_known_index = session.first_known_index(); + const firstKnownIndex = session.first_known_index(); const sessionData = { algorithm: olmlib.MEGOLM_ALGORITHM, @@ -993,7 +999,7 @@ Crypto.prototype._backupPayloadForSession = function( }; const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); return { - first_message_index: first_known_index, + first_message_index: firstKnownIndex, forwarded_count: forwardingCurve25519KeyChain.length, is_verified: false, // FIXME: how do we determine this? session_data: encrypted, From 54c443ac68a2b386f52bd03e1df2fbd93aed1d73 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Sep 2018 14:48:02 +0100 Subject: [PATCH 253/472] Make tests pass --- spec/unit/crypto/backup.spec.js | 5 ++++- src/client.js | 9 +++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index f832f0511..8763d2d8b 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -64,6 +64,9 @@ describe("MegolmBackup", function() { mockCrypto.backupKey.set_recipient_key( "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK" ); + mockCrypto.backupInfo = { + version: 1, + }; mockStorage = new MockStorageApi(); sessionStore = new WebStorageSessionStore(mockStorage); @@ -145,7 +148,7 @@ describe("MegolmBackup", function() { return event.attemptDecryption(mockCrypto).then(() => { return megolmDecryption.onRoomKeyEvent(event); }).then(() => { - expect(mockBaseApis.sendKeyBackup).toHaveBeenCalled(); + expect(mockCrypto.backupGroupSession).toHaveBeenCalled(); }); }); }); diff --git a/src/client.js b/src/client.js index c6d7e9f9e..4838493e6 100644 --- a/src/client.js +++ b/src/client.js @@ -1045,10 +1045,11 @@ MatrixClient.prototype.restoreKeyBackups = function( )); } else { totalKeyCount = 1; - const key = keyFromRecoverySession(res, decryption, keys); - key.room_id = targetRoomId; - key.session_id = targetSessionId; - keys.push(key); + keyFromRecoverySession(res, decryption, keys); + if (keys.length) { + keys[0].room_id = targetRoomId; + keys[0].session_id = targetSessionId; + } } return this.importRoomKeys(keys); From e4bb37b1a80090ef406016759ea30a90927985b1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Sep 2018 14:53:59 +0100 Subject: [PATCH 254/472] Fix lint mostly --- spec/unit/crypto/backup.spec.js | 40 +++++++++------------------------ 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 8763d2d8b..ad8bc9c6d 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -56,13 +56,13 @@ describe("MegolmBackup", function() { let sessionStore; let cryptoStore; let megolmDecryption; - beforeEach(function () { + beforeEach(function() { testUtils.beforeEach(this); // eslint-disable-line no-invalid-this mockCrypto = testUtils.mock(Crypto, 'Crypto'); mockCrypto.backupKey = new global.Olm.PkEncryption(); mockCrypto.backupKey.set_recipient_key( - "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK" + "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK", ); mockCrypto.backupInfo = { version: 1, @@ -125,24 +125,6 @@ describe("MegolmBackup", function() { return Promise.resolve(decryptedData); }; - const sessionId = groupSession.session_id(); - const cipherText = groupSession.encrypt(JSON.stringify({ - room_id: ROOM_ID, - content: 'testytest', - })); - const msgevent = new MatrixEvent({ - type: 'm.room.encrypted', - room_id: ROOM_ID, - content: { - algorithm: 'm.megolm.v1.aes-sha2', - sender_key: "SENDER_CURVE25519", - session_id: sessionId, - ciphertext: cipherText, - }, - event_id: "$event1", - origin_server_ts: 1507753886000, - }); - mockBaseApis.sendKeyBackup = expect.createSpy(); return event.attemptDecryption(mockCrypto).then(() => { @@ -153,7 +135,7 @@ describe("MegolmBackup", function() { }); }); - describe("restore", function () { + describe("restore", function() { let client; beforeEach(function() { @@ -163,9 +145,9 @@ describe("MegolmBackup", function() { ].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", + "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)); @@ -196,7 +178,7 @@ describe("MegolmBackup", function() { return client.initCrypto(); }); - it('can restore from backup', function () { + it('can restore from backup', function() { const event = new MatrixEvent({ type: 'm.room.encrypted', room_id: '!ROOM:ID', @@ -206,12 +188,12 @@ describe("MegolmBackup", function() { session_id: 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N' + 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl' - + 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs' + + 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs', }, event_id: '$event1', origin_server_ts: 1507753886000, }); - client._http.authedRequest = function () { + client._http.authedRequest = function() { return Promise.resolve({ first_message_index: 0, forwarded_count: 0, @@ -230,14 +212,14 @@ describe("MegolmBackup", function() { + 'iie8PHD8mj/5Y0GLqrac4CD6+Mop7eUTzVovprjg', mac: '5lxYBHQU80M', ephemeral: '/Bn0A4UMFwJaDDvh0aEk1XZj3k1IfgCxgFY9P9a0b14', - } + }, }); }; return client.restoreKeyBackups( "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", ROOM_ID, - 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc' + 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', ).then(() => { return megolmDecryption.decryptEvent(event); }).then((res) => { From 0bad7b213e705857b38e85912726b84abe7d4790 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Sep 2018 14:56:11 +0100 Subject: [PATCH 255/472] 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. --- src/sync.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/sync.js b/src/sync.js index 2baf78215..bcb131c59 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1099,17 +1099,6 @@ SyncApi.prototype._processSyncResponse = async function( async function processRoomEvent(e) { client.emit("event", e); if (e.isState() && e.getType() == "m.room.encryption" && self.opts.crypto) { - - /* - // XXX: get device - if (!device.getSuggestedKeyRestore() && - !device.backupKey && !device.selfCrossSigs.length) - { - client.emit("crypto.suggestKeyRestore"); - device.setSuggestedKeyRestore(true); - } - */ - await self.opts.crypto.onCryptoEvent(e); } } From a78825eff9fa5b7d119bd93ce7a1408656263d55 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Sep 2018 15:06:28 +0100 Subject: [PATCH 256/472] Bump to Olm 2.3.0 for PkEncryption --- travis.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/travis.sh b/travis.sh index 68d915def..4c47f00e7 100755 --- a/travis.sh +++ b/travis.sh @@ -5,7 +5,7 @@ set -ex npm run 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 install https://matrix.org/packages/npm/olm/olm-2.3.0.tgz npm run test From 1b62a21dbd37c9d4b46bddb4006e30368b8c0c2f Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Sep 2018 16:12:37 +0100 Subject: [PATCH 257/472] Free PkEncryption/Decryption objects --- src/client.js | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/client.js b/src/client.js index 4838493e6..7a1a0748e 100644 --- a/src/client.js +++ b/src/client.js @@ -845,6 +845,7 @@ MatrixClient.prototype.enableKeyBackup = function(info) { } this._crypto.backupInfo = info; + if (this._crypto.backupKey) this._crypto.backupKey.free(); this._crypto.backupKey = new global.Olm.PkEncryption(); this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); @@ -860,6 +861,7 @@ MatrixClient.prototype.disableKeyBackup = function() { } this._crypto.backupInfo = null; + if (this._crypto.backupKey) this._crypto.backupKey.free(); this._crypto.backupKey = null; this.emit('keyBackupStatus', false); @@ -878,16 +880,20 @@ MatrixClient.prototype.prepareKeyBackupVersion = function() { } const decryption = new global.Olm.PkDecryption(); - const publicKey = decryption.generate_key(); - return { - algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, - auth_data: { - public_key: publicKey, - }, - // FIXME: pickle isn't the right thing to use, but we don't have - // anything else yet, so use it for now - recovery_key: decryption.pickle("secret_key"), - }; + try { + const publicKey = decryption.generate_key(); + return { + algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, + auth_data: { + public_key: publicKey, + }, + // FIXME: pickle isn't the right thing to use, but we don't have + // anything else yet, so use it for now + recovery_key: decryption.pickle("secret_key"), + }; + } finally { + decryption.free(); + } }; /** @@ -1014,15 +1020,20 @@ MatrixClient.prototype.restoreKeyBackups = function( if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } - - // FIXME: see the FIXME in createKeyBackupVersion - const decryption = new global.Olm.PkDecryption(); - decryption.unpickle("secret_key", decryptionKey); - let totalKeyCount = 0; const keys = []; const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version); + + // FIXME: see the FIXME in createKeyBackupVersion + const decryption = new global.Olm.PkDecryption(); + try { + decryption.unpickle("secret_key", decryptionKey); + } catch(e) { + decryption.free(); + throw e; + } + return this._http.authedRequest( undefined, "GET", path.path, path.queryData, ).then((res) => { @@ -1055,6 +1066,8 @@ MatrixClient.prototype.restoreKeyBackups = function( return this.importRoomKeys(keys); }).then(() => { return {total: totalKeyCount, imported: keys.length}; + }).finally(() => { + decryption.free(); }); }; From 2f4c1dfcc4d3b5cd4ff5765e757cfdb6a9de026a Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Sep 2018 17:33:47 +0100 Subject: [PATCH 258/472] Test all 3 code paths on backup restore --- spec/unit/crypto/backup.spec.js | 95 +++++++++++++++++++++------------ src/client.js | 44 +++++++-------- 2 files changed, 82 insertions(+), 57 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index ad8bc9c6d..25f6c112a 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -43,6 +43,42 @@ const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; const ROOM_ID = '!ROOM:ID'; +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: 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', + 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', + }, +}; + describe("MegolmBackup", function() { if (!global.Olm) { console.warn('Not running megolm backup unit tests: libolm not present'); @@ -179,41 +215,8 @@ describe("MegolmBackup", function() { }); it('can restore from backup', function() { - const event = new MatrixEvent({ - type: 'm.room.encrypted', - room_id: '!ROOM:ID', - content: { - algorithm: 'm.megolm.v1.aes-sha2', - sender_key: 'SENDER_CURVE25519', - session_id: 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', - ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N' - + 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl' - + 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs', - }, - event_id: '$event1', - origin_server_ts: 1507753886000, - }); client._http.authedRequest = function() { - return Promise.resolve({ - 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', - }, - }); + return Promise.resolve(KEY_BACKUP_DATA); }; return client.restoreKeyBackups( "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" @@ -221,7 +224,29 @@ describe("MegolmBackup", function() { ROOM_ID, 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', ).then(() => { - return megolmDecryption.decryptEvent(event); + 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: { + 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc': KEY_BACKUP_DATA, + }, + }, + } + }); + }; + return client.restoreKeyBackups( + "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" + + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", + ).then(() => { + return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); }).then((res) => { expect(res.clearEvent.content).toEqual('testytest'); }); diff --git a/src/client.js b/src/client.js index 7a1a0748e..c864bf490 100644 --- a/src/client.js +++ b/src/client.js @@ -68,29 +68,27 @@ try { console.warn("Unable to load crypto module: crypto will be disabled: " + e); } -function keysFromRecoverySession(sessions, decryptionKey, roomId, keys) { +function keysFromRecoverySession(sessions, decryptionKey, roomId) { + const keys = []; for (const [sessionId, sessionData] of Object.entries(sessions)) { try { - const decrypted = keyFromRecoverySession(sessionData, decryptionKey, keys); + const decrypted = keyFromRecoverySession(sessionData, decryptionKey); decrypted.session_id = sessionId; decrypted.room_id = roomId; - return decrypted; + keys.push(decrypted); } catch (e) { console.log("Failed to decrypt session from backup"); } } + return keys; } -function keyFromRecoverySession(session, decryptionKey, keys) { - try { - keys.push(JSON.parse(decryptionKey.decrypt( - session.session_data.ephemeral, - session.session_data.mac, - session.session_data.ciphertext, - ))); - } catch (e) { - console.log("Failed to decrypt key from backup", e); - } +function keyFromRecoverySession(session, decryptionKey) { + return JSON.parse(decryptionKey.decrypt( + session.session_data.ephemeral, + session.session_data.mac, + session.session_data.ciphertext, + )); } /** @@ -1021,7 +1019,7 @@ MatrixClient.prototype.restoreKeyBackups = function( throw new Error("End-to-end encryption disabled"); } let totalKeyCount = 0; - const keys = []; + let keys = []; const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version); @@ -1042,8 +1040,7 @@ MatrixClient.prototype.restoreKeyBackups = function( if (!roomData.sessions) continue; totalKeyCount += Object.keys(roomData.sessions).length; - const roomKeys = []; - keysFromRecoverySession(roomData.sessions, decryption, roomId, roomKeys); + const roomKeys = keysFromRecoverySession(roomData.sessions, decryption, roomId, roomKeys); for (const k of roomKeys) { k.room_id = roomId; keys.push(k); @@ -1051,15 +1048,18 @@ MatrixClient.prototype.restoreKeyBackups = function( } } else if (res.sessions) { totalKeyCount = Object.keys(res.sessions).length; - keys.push(...keysFromRecoverySession( + keys = keysFromRecoverySession( res.sessions, decryption, targetRoomId, keys, - )); + ); } else { totalKeyCount = 1; - keyFromRecoverySession(res, decryption, keys); - if (keys.length) { - keys[0].room_id = targetRoomId; - keys[0].session_id = targetSessionId; + try { + const key = keyFromRecoverySession(res, decryption); + key.room_id = targetRoomId; + key.session_id = targetSessionId; + keys.push(key); + } catch (e) { + console.log("Failed to decrypt session from backup"); } } From a91fa5917465bdfb1d2026d3af51bc5f2e5af152 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 19 Sep 2018 18:14:18 +0200 Subject: [PATCH 259/472] fix display name disambiguation with LL --- src/models/room-state.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/room-state.js b/src/models/room-state.js index 85d8a744f..52bc5ed36 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -180,7 +180,7 @@ RoomState.prototype.getSentinelMember = function(userId) { sentinel = new RoomMember(this.roomId, userId); const member = this.members[userId]; if (member) { - sentinel.setMembershipEvent(member.events.member); + sentinel.setMembershipEvent(member.events.member, this); } this._sentinels[userId] = sentinel; } @@ -501,7 +501,7 @@ RoomState.prototype._setOutOfBandMember = function(stateEvent) { } const member = this._getOrCreateMember(userId, stateEvent); - member.setMembershipEvent(stateEvent); + member.setMembershipEvent(stateEvent, this); // needed to know which members need to be stored seperately // as the are not part of the sync accumulator // this is cleared by setMembershipEvent so when it's updated through /sync From a08a3078dae5da90f5a2edbf380a8bbbbff44b28 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 20 Sep 2018 11:20:49 +0100 Subject: [PATCH 260/472] Revert "room name should only take canonical alias into account" --- spec/unit/room.spec.js | 8 ++++---- src/models/room.js | 10 +++++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/spec/unit/room.spec.js b/spec/unit/room.spec.js index 6aac51626..4a8ea7e59 100644 --- a/spec/unit/room.spec.js +++ b/spec/unit/room.spec.js @@ -863,24 +863,24 @@ describe("Room", function() { expect(name.indexOf(userB)).toNotEqual(-1, name); }); - it("should not show the room alias if one exists for private " + + it("should show the room alias if one exists for private " + "(invite join_rules) rooms if a room name doesn't exist.", function() { const alias = "#room_alias:here"; setJoinRule("invite"); setAliases([alias, "#another:one"]); room.recalculate(); const name = room.name; - expect(name).toEqual("Empty room"); + expect(name).toEqual(alias); }); - it("should not show the room alias if one exists for public " + + it("should show the room alias if one exists for public " + "(public join_rules) rooms if a room name doesn't exist.", function() { const alias = "#room_alias:here"; setJoinRule("public"); setAliases([alias, "#another:one"]); room.recalculate(); const name = room.name; - expect(name).toEqual("Empty room"); + expect(name).toEqual(alias); }); it("should show the room name if one exists for private " + diff --git a/src/models/room.js b/src/models/room.js index fd9fcc044..c99d87ba5 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1525,7 +1525,15 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { } } - const alias = room.getCanonicalAlias(); + let alias = room.getCanonicalAlias(); + + if (!alias) { + const aliases = room.getAliases(); + + if (aliases.length) { + alias = aliases[0]; + } + } if (alias) { return alias; } From d28f829b1cf825f2707fe0c2ee9b6871738aedee Mon Sep 17 00:00:00 2001 From: Ben Parsons Date: Thu, 20 Sep 2018 13:12:50 +0100 Subject: [PATCH 261/472] add new examples, to be expanded into a post --- README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/README.md b/README.md index ec11da0a3..15eae610d 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,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 +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. Access this via `client.store`. For example to iterate through the currently stored timeline for a room: + +```javascript +Object.keys(client.store.rooms).forEach((roomId) => { + client.store.rooms[roomId].timeline.forEach(t => { + console.log(t.event); + }); +}); +``` What does this SDK do? ---------------------- From 76175abea2d4c3630d4a96ed78274686179989ea Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 24 Sep 2018 17:45:33 +0100 Subject: [PATCH 262/472] allow storing client options in indexeddb so we can tell what options the sync data was created with --- src/store/indexeddb-local-backend.js | 35 ++++++++++++++++++++++++++- src/store/indexeddb-remote-backend.js | 8 ++++++ src/store/indexeddb-store-worker.js | 6 +++++ src/store/indexeddb.js | 8 ++++++ src/store/memory.js | 9 +++++++ 5 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index 07d9f8139..67d2185b8 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -19,7 +19,7 @@ import Promise from 'bluebird'; import SyncAccumulator from "../sync-accumulator"; import utils from "../utils"; -const VERSION = 2; +const VERSION = 3; function createDatabase(db) { // Make user store, clobber based on user ID. (userId property of User objects) @@ -41,6 +41,12 @@ function upgradeSchemaV2(db) { oobMembersStore.createIndex("room", "room_id"); } +function upgradeSchemaV3(db) { + const clientOptionsStore = 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. @@ -158,6 +164,9 @@ LocalIndexedDBStoreBackend.prototype = { if (oldVersion < 2) { upgradeSchemaV2(db); } + if (oldVersion < 3) { + upgradeSchemaV3(db); + } // Expand as needed. }; @@ -529,6 +538,30 @@ 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: function(options) { + return Promise.resolve().then(() => { + 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 + return txnAsPromise(txn); + }); + } }; export default LocalIndexedDBStoreBackend; diff --git a/src/store/indexeddb-remote-backend.js b/src/store/indexeddb-remote-backend.js index 85f07f86b..751d155b4 100644 --- a/src/store/indexeddb-remote-backend.js +++ b/src/store/indexeddb-remote-backend.js @@ -114,6 +114,14 @@ RemoteIndexedDBStoreBackend.prototype = { 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. * @return {Promise} A list of presence events in their raw form. diff --git a/src/store/indexeddb-store-worker.js b/src/store/indexeddb-store-worker.js index adfc3535b..060d3c8f8 100644 --- a/src/store/indexeddb-store-worker.js +++ b/src/store/indexeddb-store-worker.js @@ -101,6 +101,12 @@ class IndexedDBStoreWorker { 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) { diff --git a/src/store/indexeddb.js b/src/store/indexeddb.js index 0de57c89a..479b79e8c 100644 --- a/src/store/indexeddb.js +++ b/src/store/indexeddb.js @@ -246,4 +246,12 @@ 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; diff --git a/src/store/memory.js b/src/store/memory.js index a5f72af4f..a4d2ddb6b 100644 --- a/src/store/memory.js +++ b/src/store/memory.js @@ -55,6 +55,7 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) { this._oobMembers = { // roomId: [member events] }; + this._clientOptions = {}; }; module.exports.MatrixInMemoryStore.prototype = { @@ -402,4 +403,12 @@ module.exports.MatrixInMemoryStore.prototype = { this._oobMembers[roomId] = membershipEvents; return Promise.resolve(); }, + + getClientOptions: function() { + return this._clientOptions; + }, + + storeClientOptions: function(options) { + return this._clientOptions = Object.assign({}, options); + }, }; From 28184b4a296fe8695d54129bc4023b0c2f7e7e58 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Sep 2018 15:32:10 +0100 Subject: [PATCH 263/472] check if lazy loading was enabled before in startClient --- src/client.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 8f0ca893d..973263429 100644 --- a/src/client.js +++ b/src/client.js @@ -3115,7 +3115,16 @@ MatrixClient.prototype.startClient = async function(opts) { opts.lazyLoadMembers = false; } } - + // need to vape the store when enabling LL and wasn't enabled before + let hadLLEnabledBefore = false; + const prevClientOptions = await this.store.getClientOptions(); + if (prevClientOptions) { + hadLLEnabledBefore = !!prevClientOptions.lazyLoadMembers; + } + if (!hadLLEnabledBefore && opts.lazyLoadMembers) { + await this.store.deleteAllData(); + throw new Error("vaped the store, you need to resync"); + } if (opts.lazyLoadMembers && this._crypto) { this._crypto.enableLazyLoading(); } @@ -3128,6 +3137,7 @@ MatrixClient.prototype.startClient = async function(opts) { return this._canResetTimelineCallback(roomId); }; this._clientOpts = opts; + await this.store.storeClientOptions(this._clientOpts); this._syncApi = new SyncApi(this, opts); this._syncApi.sync(); From 0519c4c6b1a2b4696899b923c57c5207b9ef7602 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Sep 2018 15:53:14 +0100 Subject: [PATCH 264/472] await startClient and use promises also so error gets shown --- spec/integ/matrix-client-opts.spec.js | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/spec/integ/matrix-client-opts.spec.js b/spec/integ/matrix-client-opts.spec.js index c7388678a..201ae7960 100644 --- a/spec/integ/matrix-client-opts.spec.js +++ b/spec/integ/matrix-client-opts.spec.js @@ -94,7 +94,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 +110,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, + ); }); }); From b0dbb20e22d32e64c805a16ed7a62233cfb564d4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Sep 2018 15:53:40 +0100 Subject: [PATCH 265/472] fixup of in memory stores --- src/store/memory.js | 5 +++-- src/store/stub.js | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/store/memory.js b/src/store/memory.js index a4d2ddb6b..33c4ff9c6 100644 --- a/src/store/memory.js +++ b/src/store/memory.js @@ -405,10 +405,11 @@ module.exports.MatrixInMemoryStore.prototype = { }, getClientOptions: function() { - return this._clientOptions; + return Promise.resolve(this._clientOptions); }, storeClientOptions: function(options) { - return this._clientOptions = Object.assign({}, options); + this._clientOptions = Object.assign({}, options); + return Promise.resolve(); }, }; diff --git a/src/store/stub.js b/src/store/stub.js index d0c2cabc5..2582a9802 100644 --- a/src/store/stub.js +++ b/src/store/stub.js @@ -276,6 +276,14 @@ StubStore.prototype = { clearOutOfBandMembers: function() { return Promise.resolve(); }, + + getClientOptions: function() { + return Promise.resolve(); + }, + + storeClientOptions: function() { + return Promise.resolve(); + }, }; /** Stub Store class. */ From 8f2824186a153fe27627ea4bf2f2a6a0d4059f72 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 25 Sep 2018 10:09:58 -0600 Subject: [PATCH 266/472] 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. --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6f1ecfc14..e59fdc090 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "test": "npm run test:build && npm run test:run", "check": "npm run 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": "npm run start:init && npm run 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", From c556ca40b159e141b90e542cd8f82329fdce6043 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 25 Sep 2018 17:49:54 +0100 Subject: [PATCH 267/472] 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. --- spec/unit/crypto.spec.js | 13 ++++---- spec/unit/crypto/algorithms/megolm.spec.js | 15 ++++------ src/client.js | 35 ++++++++++------------ src/crypto/OlmDevice.js | 3 -- src/crypto/index.js | 15 +++++++--- 5 files changed, 40 insertions(+), 41 deletions(-) diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js index 4d949e05e..734941cb5 100644 --- a/spec/unit/crypto.spec.js +++ b/spec/unit/crypto.spec.js @@ -1,20 +1,23 @@ "use strict"; import 'source-map-support/register'; +import Crypto from '../../lib/crypto'; +import expect from 'expect'; const sdk = require("../.."); -let Crypto; -if (sdk.CRYPTO_ENABLED) { - Crypto = require("../../lib/crypto"); -} -import expect from 'expect'; describe("Crypto", function() { if (!sdk.CRYPTO_ENABLED) { return; } + + beforeEach(function(done) { + Olm.init().then(done); + }); + it("Crypto exposes the correct olm library version", function() { + console.log(Crypto); expect(Crypto.getOlmVersion()[0]).toEqual(2); }); }); diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index cf8e58f2e..db28e3a51 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -13,14 +13,8 @@ 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']; @@ -69,7 +63,8 @@ describe("MegolmDecryption", function() { describe('receives some keys:', function() { let groupSession; - beforeEach(function() { + beforeEach(async function() { + await Olm.init(); 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); }); }); diff --git a/src/client.js b/src/client.js index 8f0ca893d..8ca115a38 100644 --- a/src/client.js +++ b/src/client.js @@ -45,6 +45,8 @@ const ContentHelpers = require("./content-helpers"); import ReEmitter from './ReEmitter'; import RoomList from './crypto/RoomList'; +import Crypto from './crypto'; +import { isCryptoAvailable } from './crypto'; const LAZY_LOADING_MESSAGES_FILTER = { lazy_load_members: true, @@ -58,14 +60,7 @@ const LAZY_LOADING_SYNC_FILTER = { const SCROLLBACK_DELAY_MS = 3000; -let CRYPTO_ENABLED = false; - -try { - var Crypto = require("./crypto"); - CRYPTO_ENABLED = true; -} catch (e) { - console.warn("Unable to load crypto module: crypto will be disabled: " + e); -} +let CRYPTO_ENABLED = isCryptoAvailable(); /** * Construct a Matrix Client. Only directly construct this if you want to use @@ -140,6 +135,8 @@ function MatrixClient(opts) { MatrixBaseApis.call(this, opts); + this.olmVersion = null; // Populated after initCrypto is done + this.reEmitter = new ReEmitter(this); this.store = opts.store || new StubStore(); @@ -192,10 +189,6 @@ function MatrixClient(opts) { this._forceTURN = opts.forceTURN || false; - if (CRYPTO_ENABLED) { - this.olmVersion = Crypto.getOlmVersion(); - } - // List of which rooms have encryption enabled: separate from crypto because // we still want to know which rooms are encrypted even if crypto is disabled: // we don't want to start sending unencrypted events to them. @@ -385,6 +378,14 @@ MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) { * successfully initialised. */ MatrixClient.prototype.initCrypto = async function() { + if (!isCryptoAvailable()) { + throw new Error( + `End-to-end encryption not supported in this js-sdk build: did ` + + `you remember to load the olm library?`, + ); + return; + } + if (this._crypto) { console.warn("Attempt to re-initialise e2e encryption on MatrixClient"); return; @@ -402,13 +403,6 @@ MatrixClient.prototype.initCrypto = async function() { // initialise the list of encrypted rooms (whether or not crypto is enabled) await this._roomList.init(); - if (!CRYPTO_ENABLED) { - throw new Error( - `End-to-end encryption not supported in this js-sdk build: did ` + - `you remember to load the olm library?`, - ); - } - const userId = this.getUserId(); if (userId === null) { throw new Error( @@ -440,6 +434,9 @@ MatrixClient.prototype.initCrypto = async function() { await crypto.init(); + this.olmVersion = Crypto.getOlmVersion(); + + // if crypto initialisation was successful, tell it to attach its event // handlers. crypto.registerEventHandlers(this); diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index cda14779c..b8c70cd0c 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -23,9 +23,6 @@ import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; * @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 diff --git a/src/crypto/index.js b/src/crypto/index.js index f00477d4b..0fe33663f 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -36,6 +36,12 @@ const DeviceList = require('./DeviceList').default; import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; +const Olm = global.Olm; + +export function isCryptoAvailable() { + return Boolean(Olm); +} + /** * Cryptography bits * @@ -62,7 +68,7 @@ import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; * * @param {RoomList} roomList An initialised RoomList object */ -function Crypto(baseApis, sessionStore, userId, deviceId, +export default function Crypto(baseApis, sessionStore, userId, deviceId, clientStore, cryptoStore, roomList) { this._baseApis = baseApis; this._sessionStore = sessionStore; @@ -124,6 +130,10 @@ utils.inherits(Crypto, EventEmitter); * Returns a promise which resolves once the crypto module is ready for use. */ Crypto.prototype.init = async function() { + // Olm is just an object with a .then, not a fully-fledged promise, so + // pass it into bluebird to make it a proper promise. + await Olm.init(); + const sessionStoreHasAccount = Boolean(this._sessionStore.getEndToEndAccount()); let cryptoStoreHasAccount; await this._cryptoStore.doTxn( @@ -1518,6 +1528,3 @@ class IncomingRoomKeyRequestCancellation { * @event module:client~MatrixClient#"crypto.warning" * @param {string} type One of the strings listed above */ - -/** */ -module.exports = Crypto; From 63cc3fd890b9927c489d65e50895a0ba74196ca4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 25 Sep 2018 18:14:11 +0100 Subject: [PATCH 268/472] lint --- spec/unit/crypto.spec.js | 1 + spec/unit/crypto/algorithms/megolm.spec.js | 2 ++ src/client.js | 3 +-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js index 734941cb5..1b28ad683 100644 --- a/spec/unit/crypto.spec.js +++ b/spec/unit/crypto.spec.js @@ -6,6 +6,7 @@ import expect from 'expect'; const sdk = require("../.."); +const Olm = global.Olm; describe("Crypto", function() { if (!sdk.CRYPTO_ENABLED) { diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index db28e3a51..6c777859e 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -21,6 +21,8 @@ const MegolmDecryption = algorithms.DECRYPTION_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'); diff --git a/src/client.js b/src/client.js index 8ca115a38..1a05998eb 100644 --- a/src/client.js +++ b/src/client.js @@ -60,7 +60,7 @@ const LAZY_LOADING_SYNC_FILTER = { const SCROLLBACK_DELAY_MS = 3000; -let CRYPTO_ENABLED = isCryptoAvailable(); +const CRYPTO_ENABLED = isCryptoAvailable(); /** * Construct a Matrix Client. Only directly construct this if you want to use @@ -383,7 +383,6 @@ MatrixClient.prototype.initCrypto = async function() { `End-to-end encryption not supported in this js-sdk build: did ` + `you remember to load the olm library?`, ); - return; } if (this._crypto) { From 1fd8c43d942edd0d3685ca3fb39db3a56f5d037c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 25 Sep 2018 18:50:09 +0100 Subject: [PATCH 269/472] fix tests --- spec/unit/matrix-client.spec.js | 49 ++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/spec/unit/matrix-client.spec.js b/spec/unit/matrix-client.spec.js index 2fc5d2cf0..dae9aaa3a 100644 --- a/spec/unit/matrix-client.spec.js +++ b/spec/unit/matrix-client.spec.js @@ -139,6 +139,8 @@ 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)); client = new MatrixClient({ baseUrl: "https://my.home.server", idBaseUrl: identityServerUrl, @@ -182,7 +184,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 +193,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 +213,20 @@ 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) => { + throw new Error("fail!!"); + 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 +269,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); }); From 19be3dd852c10c33c2b47b3fc353dcdd2ff2f289 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Sep 2018 10:13:40 +0100 Subject: [PATCH 270/472] fix lint --- src/store/indexeddb-local-backend.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index 67d2185b8..7a184e1d1 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -42,8 +42,8 @@ function upgradeSchemaV2(db) { } function upgradeSchemaV3(db) { - const clientOptionsStore = db.createObjectStore( - "client_options", { keyPath: ["clobber"]}); + db.createObjectStore("client_options", + { keyPath: ["clobber"]}); } @@ -557,11 +557,11 @@ LocalIndexedDBStoreBackend.prototype = { const store = txn.objectStore("client_options"); store.put({ clobber: "-", // constant key so will always clobber - options: options + options: options, }); // put == UPSERT return txnAsPromise(txn); }); - } + }, }; export default LocalIndexedDBStoreBackend; From 2560ba2980b843cc109e390be3a16a0ed9274784 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Sep 2018 10:37:52 +0100 Subject: [PATCH 271/472] dont clear the store if its a brand new one --- spec/unit/matrix-client.spec.js | 1 + src/client.js | 30 ++++++++++++++++++++------- src/store/indexeddb-local-backend.js | 6 ++++++ src/store/indexeddb-remote-backend.js | 5 ++++- src/store/indexeddb-store-worker.js | 3 +++ src/store/indexeddb.js | 5 +++++ src/store/memory.js | 4 ++++ src/store/stub.js | 5 +++++ 8 files changed, 50 insertions(+), 9 deletions(-) diff --git a/spec/unit/matrix-client.spec.js b/spec/unit/matrix-client.spec.js index dae9aaa3a..c37f99582 100644 --- a/spec/unit/matrix-client.spec.js +++ b/spec/unit/matrix-client.spec.js @@ -141,6 +141,7 @@ describe("MatrixClient", function() { 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, diff --git a/src/client.js b/src/client.js index 973263429..fe7892bf1 100644 --- a/src/client.js +++ b/src/client.js @@ -3116,14 +3116,12 @@ MatrixClient.prototype.startClient = async function(opts) { } } // need to vape the store when enabling LL and wasn't enabled before - let hadLLEnabledBefore = false; - const prevClientOptions = await this.store.getClientOptions(); - if (prevClientOptions) { - hadLLEnabledBefore = !!prevClientOptions.lazyLoadMembers; - } - if (!hadLLEnabledBefore && opts.lazyLoadMembers) { - await this.store.deleteAllData(); - throw new Error("vaped the store, you need to resync"); + if (opts.lazyLoadMembers) { + const shouldClear = await this._shouldClearSyncDataIfLL(); + if (shouldClear) { + await this.store.deleteAllData(); + throw new Error("vaped the store, you need to resync"); + } } if (opts.lazyLoadMembers && this._crypto) { this._crypto.enableLazyLoading(); @@ -3143,6 +3141,22 @@ MatrixClient.prototype.startClient = async function(opts) { this._syncApi.sync(); }; +/** @return {bool} need to clear the store when enabling LL and wasn't enabled before? */ +MatrixClient.prototype._shouldClearSyncDataIfLL = async function() { + let hadLLEnabledBefore = false; + const isStoreNewlyCreated = await this.store.isNewlyCreated(); + if (!isStoreNewlyCreated) { + const prevClientOptions = await this.store.getClientOptions(); + if (prevClientOptions) { + hadLLEnabledBefore = !!prevClientOptions.lazyLoadMembers; + } + if (!hadLLEnabledBefore) { + return true; + } + } + return false; +}; + /** * High level helper method to stop the client from polling and allow a * clean shutdown. diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index 7a184e1d1..db256dead 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -129,6 +129,7 @@ const LocalIndexedDBStoreBackend = function LocalIndexedDBStoreBackend( this.db = null; this._disconnected = true; this._syncAccumulator = new SyncAccumulator(); + this._isNewlyCreated = false; }; @@ -159,6 +160,7 @@ LocalIndexedDBStoreBackend.prototype = { `LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`, ); if (oldVersion < 1) { // The database did not previously exist. + this._isNewlyCreated = true; createDatabase(db); } if (oldVersion < 2) { @@ -194,6 +196,10 @@ LocalIndexedDBStoreBackend.prototype = { return this._init(); }); }, + /** @return {bool} whether or not the database was newly created in this session. */ + isNewlyCreated: function() { + return this._isNewlyCreated; + }, /** * Having connected, load initial data from the database and prepare for use diff --git a/src/store/indexeddb-remote-backend.js b/src/store/indexeddb-remote-backend.js index 751d155b4..07ee18516 100644 --- a/src/store/indexeddb-remote-backend.js +++ b/src/store/indexeddb-remote-backend.js @@ -65,7 +65,10 @@ RemoteIndexedDBStoreBackend.prototype = { clearDatabase: function() { return this._ensureStarted().then(() => this._doCmd('clearDatabase')); }, - + /** @return {Promise} 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 diff --git a/src/store/indexeddb-store-worker.js b/src/store/indexeddb-store-worker.js index 060d3c8f8..d9f53a053 100644 --- a/src/store/indexeddb-store-worker.js +++ b/src/store/indexeddb-store-worker.js @@ -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 diff --git a/src/store/indexeddb.js b/src/store/indexeddb.js index 479b79e8c..b8f47ad89 100644 --- a/src/store/indexeddb.js +++ b/src/store/indexeddb.js @@ -146,6 +146,11 @@ IndexedDBStore.prototype.getSavedSync = function() { return this.backend.getSavedSync(); }; +/** @return {Promise} 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. diff --git a/src/store/memory.js b/src/store/memory.js index 33c4ff9c6..006742223 100644 --- a/src/store/memory.js +++ b/src/store/memory.js @@ -68,6 +68,10 @@ module.exports.MatrixInMemoryStore.prototype = { return this.syncToken; }, + /** @return {Promise} whether or not the database was newly created in this session. */ + isNewlyCreated: function() { + return Promise.resolve(true); + }, /** * Set the token to stream from. diff --git a/src/store/stub.js b/src/store/stub.js index 2582a9802..f09dad6d7 100644 --- a/src/store/stub.js +++ b/src/store/stub.js @@ -32,6 +32,11 @@ function StubStore() { StubStore.prototype = { + /** @return {Promise} whether or not the database was newly created in this session. */ + isNewlyCreated: function() { + return Promise.resolve(true); + }, + /** * Get the sync token. * @return {string} From 1d0791142cf2cbba54532011affd9c40d32e4bbe Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Sep 2018 11:32:11 +0100 Subject: [PATCH 272/472] all store methods should return a promise --- src/store/indexeddb-local-backend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index db256dead..f527768a2 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -198,7 +198,7 @@ LocalIndexedDBStoreBackend.prototype = { }, /** @return {bool} whether or not the database was newly created in this session. */ isNewlyCreated: function() { - return this._isNewlyCreated; + return Promise.resolve(this._isNewlyCreated); }, /** From 4e0af3eafe69b7d94200eed186c483d1a10c8d68 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Sep 2018 11:32:43 +0100 Subject: [PATCH 273/472] don't return the IDBEvent from storeClientOptions as it's not needed and not cloneable --- src/store/indexeddb-local-backend.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index f527768a2..e0ada7904 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -557,16 +557,14 @@ LocalIndexedDBStoreBackend.prototype = { }); }, - storeClientOptions: function(options) { - return Promise.resolve().then(() => { - 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 - return txnAsPromise(txn); - }); + 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); }, }; From 6dd5c6c31728c4b46c4492fe0c7014708d4ea506 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Sep 2018 11:33:19 +0100 Subject: [PATCH 274/472] fix existing missing this --- src/store/indexeddb-store-worker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/store/indexeddb-store-worker.js b/src/store/indexeddb-store-worker.js index d9f53a053..bf2c1184d 100644 --- a/src/store/indexeddb-store-worker.js +++ b/src/store/indexeddb-store-worker.js @@ -113,7 +113,7 @@ class IndexedDBStoreWorker { } if (prom === undefined) { - postMessage({ + this.postMessage({ command: 'cmd_fail', seq: msg.seq, // Can't be an Error because they're not structured cloneable From 58e3c72446c2024dfa9f1e4d6ed2e75ba07cce63 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Sep 2018 11:34:58 +0100 Subject: [PATCH 275/472] only store serializable options (string, boolean, number) --- src/client.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/client.js b/src/client.js index fe7892bf1..d2fa6af91 100644 --- a/src/client.js +++ b/src/client.js @@ -3135,8 +3135,7 @@ MatrixClient.prototype.startClient = async function(opts) { return this._canResetTimelineCallback(roomId); }; this._clientOpts = opts; - await this.store.storeClientOptions(this._clientOpts); - + await this._storeClientOptions(this._clientOpts); this._syncApi = new SyncApi(this, opts); this._syncApi.sync(); }; @@ -3157,6 +3156,25 @@ MatrixClient.prototype._shouldClearSyncDataIfLL = async function() { return false; }; +/** + * store client options with boolean/string/numeric values + * to know in the next session what flags the sync data was + * created with (e.g. lazy loading) + * @param {object} opts the complete set of client options + * @return {Promise} for store operation */ +MatrixClient.prototype._storeClientOptions = function(opts) { + const primTypes = ["boolean", "string", "number"]; + const serializableOpts = Object.entries(opts) + .filter(([key, value]) => { + return primTypes.includes(typeof value); + }) + .reduce((obj, [key, value]) => { + obj[key] = value; + return obj; + }, {}); + return this.store.storeClientOptions(serializableOpts); +}; + /** * High level helper method to stop the client from polling and allow a * clean shutdown. From fe21972f4a9f31637bef5b1fab7d0d8972b7e444 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 26 Sep 2018 11:36:06 +0100 Subject: [PATCH 276/472] Update mocha to v5 Mostly to get the non-vulnerable version of node-growl --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e59fdc090..376b28ec6 100644 --- a/package.json +++ b/package.json @@ -74,8 +74,8 @@ "jsdoc": "^3.5.5", "lolex": "^1.5.2", "matrix-mock-request": "^1.2.0", - "mocha": "^3.2.0", - "mocha-jenkins-reporter": "^0.3.6", + "mocha": "^5.2.0", + "mocha-jenkins-reporter": "^0.4.0", "rimraf": "^2.5.4", "source-map-support": "^0.4.11", "sourceify": "^0.1.0", From 54bff814708ccab4d1ab3a3154150101702c276c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Sep 2018 12:39:56 +0100 Subject: [PATCH 277/472] 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 --- src/client.js | 26 +++++++++++++------------- src/errors.js | 23 +++++++++++++++++++++++ src/matrix.js | 2 ++ 3 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 src/errors.js diff --git a/src/client.js b/src/client.js index d2fa6af91..300e747c4 100644 --- a/src/client.js +++ b/src/client.js @@ -44,6 +44,7 @@ const ContentHelpers = require("./content-helpers"); import ReEmitter from './ReEmitter'; import RoomList from './crypto/RoomList'; +import {InvalidStoreError} from './errors'; const LAZY_LOADING_MESSAGES_FILTER = { @@ -3116,12 +3117,10 @@ MatrixClient.prototype.startClient = async function(opts) { } } // need to vape the store when enabling LL and wasn't enabled before - if (opts.lazyLoadMembers) { - const shouldClear = await this._shouldClearSyncDataIfLL(); - if (shouldClear) { - await this.store.deleteAllData(); - throw new Error("vaped the store, you need to resync"); - } + const shouldClear = await this._shouldClearSyncDataIfLLToggled(opts.lazyLoadMembers); + if (shouldClear) { + const reason = InvalidStoreError.TOGGLED_LAZY_LOADING; + throw new InvalidStoreError(reason); } if (opts.lazyLoadMembers && this._crypto) { this._crypto.enableLazyLoading(); @@ -3140,18 +3139,19 @@ MatrixClient.prototype.startClient = async function(opts) { this._syncApi.sync(); }; -/** @return {bool} need to clear the store when enabling LL and wasn't enabled before? */ -MatrixClient.prototype._shouldClearSyncDataIfLL = async function() { - let hadLLEnabledBefore = false; +/** @return {bool} need to clear the store when toggling LL compared to previous session? */ +MatrixClient.prototype._shouldClearSyncDataIfLLToggled = 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.store.isNewlyCreated(); if (!isStoreNewlyCreated) { const prevClientOptions = await this.store.getClientOptions(); if (prevClientOptions) { - hadLLEnabledBefore = !!prevClientOptions.lazyLoadMembers; - } - if (!hadLLEnabledBefore) { - return true; + lazyLoadMembersBefore = !!prevClientOptions.lazyLoadMembers; } + return lazyLoadMembersBefore !== lazyLoadMembers; } return false; }; diff --git a/src/errors.js b/src/errors.js new file mode 100644 index 000000000..71e91ee3c --- /dev/null +++ b/src/errors.js @@ -0,0 +1,23 @@ +// can't just do InvalidStoreError extends Error +// because of http://babeljs.io/docs/usage/caveats/#classes +function InvalidStoreError(reason) { + const message = `Store is invalid because ${reason}, please delete all data and retry`; + const instance = Reflect.construct(Error, [message]); + Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this)); + instance.reason = reason; + 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); + +module.exports.InvalidStoreError = InvalidStoreError; diff --git a/src/matrix.js b/src/matrix.js index bcc88e0e9..47791472e 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -34,6 +34,8 @@ 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. */ From b7b9c67259efe5828c3fed2f342d9057cea0fc8a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Sep 2018 12:49:26 +0100 Subject: [PATCH 278/472] fix lint --- src/errors.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/errors.js b/src/errors.js index 71e91ee3c..d26a8647f 100644 --- a/src/errors.js +++ b/src/errors.js @@ -1,7 +1,8 @@ // can't just do InvalidStoreError extends Error // because of http://babeljs.io/docs/usage/caveats/#classes function InvalidStoreError(reason) { - const message = `Store is invalid because ${reason}, please delete all data and retry`; + const message = `Store is invalid because ${reason}, ` + + `please delete all data and retry`; const instance = Reflect.construct(Error, [message]); Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this)); instance.reason = reason; @@ -15,8 +16,8 @@ InvalidStoreError.prototype = Object.create(Error.prototype, { value: Error, enumerable: false, writable: true, - configurable: true - } + configurable: true, + }, }); Reflect.setPrototypeOf(InvalidStoreError, Error); From 78a5a886385a30d10068679f54889b99ad9374f3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Sep 2018 12:49:33 +0100 Subject: [PATCH 279/472] fix jsdoc lint and better naming --- src/client.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/client.js b/src/client.js index 300e747c4..ee501ec0a 100644 --- a/src/client.js +++ b/src/client.js @@ -3117,7 +3117,7 @@ MatrixClient.prototype.startClient = async function(opts) { } } // need to vape the store when enabling LL and wasn't enabled before - const shouldClear = await this._shouldClearSyncDataIfLLToggled(opts.lazyLoadMembers); + const shouldClear = await this._wasLazyLoadingToggled(opts.lazyLoadMembers); if (shouldClear) { const reason = InvalidStoreError.TOGGLED_LAZY_LOADING; throw new InvalidStoreError(reason); @@ -3139,8 +3139,11 @@ MatrixClient.prototype.startClient = async function(opts) { this._syncApi.sync(); }; -/** @return {bool} need to clear the store when toggling LL compared to previous session? */ -MatrixClient.prototype._shouldClearSyncDataIfLLToggled = async function(lazyLoadMembers) { +/** + * 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 */ +MatrixClient.prototype._wasLazyLoadingToggled = async function(lazyLoadMembers) { lazyLoadMembers = !!lazyLoadMembers; // assume it was turned off before // if we don't know any better From cba1e95d0a357ad9a10394bbb330d7bccd47d7a2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 26 Sep 2018 09:11:28 -0600 Subject: [PATCH 280/472] Revert "Add getMediaLimits to client" --- src/client.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/client.js b/src/client.js index 821add2d9..bf1edf30b 100644 --- a/src/client.js +++ b/src/client.js @@ -771,17 +771,6 @@ MatrixClient.prototype.getGroups = function() { return this.store.getGroups(); }; -/** - * Get the config for the media repository. - * @param {module:client.callback} callback Optional. - * @return {module:client.Promise} Resolves with an object containing the config. - */ -MatrixClient.prototype.getMediaConfig = function(callback) { - return this._http.requestWithPrefix( - callback, "GET", "/config", undefined, undefined, httpApi.PREFIX_MEDIA_R0, - ); -}; - // Room ops // ======== From 2d5eb920b84e7f3ab5ba247c0288307bd2a0448f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Sep 2018 16:12:30 +0100 Subject: [PATCH 281/472] pass lazy loading flag into error, to format message based on it --- src/client.js | 2 +- src/errors.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client.js b/src/client.js index ee501ec0a..d5ba14e58 100644 --- a/src/client.js +++ b/src/client.js @@ -3120,7 +3120,7 @@ MatrixClient.prototype.startClient = async function(opts) { const shouldClear = await this._wasLazyLoadingToggled(opts.lazyLoadMembers); if (shouldClear) { const reason = InvalidStoreError.TOGGLED_LAZY_LOADING; - throw new InvalidStoreError(reason); + throw new InvalidStoreError(reason, !!opts.lazyLoadMembers); } if (opts.lazyLoadMembers && this._crypto) { this._crypto.enableLazyLoading(); diff --git a/src/errors.js b/src/errors.js index d26a8647f..04e14f2c8 100644 --- a/src/errors.js +++ b/src/errors.js @@ -1,11 +1,12 @@ // can't just do InvalidStoreError extends Error // because of http://babeljs.io/docs/usage/caveats/#classes -function InvalidStoreError(reason) { +function InvalidStoreError(reason, value) { const message = `Store is invalid because ${reason}, ` + `please delete all data and retry`; const instance = Reflect.construct(Error, [message]); Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this)); instance.reason = reason; + instance.value = value; return instance; } From 33ad65a105d379b6208a950679e3c5bb80525698 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 26 Sep 2018 16:39:22 +0100 Subject: [PATCH 282/472] 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. --- src/crypto/OlmDevice.js | 30 +++++++++++------------------- src/crypto/index.js | 6 ++---- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index b8c70cd0c..043905c09 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -17,14 +17,6 @@ limitations under the License. import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; -/** - * olm.js wrapper - * - * @module crypto/OlmDevice - */ -const Olm = global.Olm; - - // 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; @@ -124,7 +116,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, @@ -158,7 +150,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() { @@ -265,7 +257,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); @@ -318,7 +310,7 @@ OlmDevice.prototype._getSession = function(deviceKey, sessionId, txn, func) { * @private */ OlmDevice.prototype._unpickleSession = function(pickledSession, func) { - const session = new Olm.Session(); + const session = new global.Olm.Session(); try { session.unpickle(this._pickleKey, pickledSession); func(session); @@ -351,7 +343,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 { @@ -463,7 +455,7 @@ 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(); @@ -507,7 +499,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, @@ -725,7 +717,7 @@ OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) { 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); @@ -741,7 +733,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); @@ -813,7 +805,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); @@ -894,7 +886,7 @@ OlmDevice.prototype.addInboundGroupSession = async function( } // new session. - const session = new Olm.InboundGroupSession(); + const session = new global.Olm.InboundGroupSession(); try { if (exportFormat) { session.import_session(sessionKey); diff --git a/src/crypto/index.js b/src/crypto/index.js index 0fe33663f..9ed3c7113 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -36,10 +36,8 @@ const DeviceList = require('./DeviceList').default; import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; -const Olm = global.Olm; - export function isCryptoAvailable() { - return Boolean(Olm); + return Boolean(global.Olm); } /** @@ -132,7 +130,7 @@ utils.inherits(Crypto, EventEmitter); Crypto.prototype.init = async function() { // Olm is just an object with a .then, not a fully-fledged promise, so // pass it into bluebird to make it a proper promise. - await Olm.init(); + await global.Olm.init(); const sessionStoreHasAccount = Boolean(this._sessionStore.getEndToEndAccount()); let cryptoStoreHasAccount; From ba39b64ced72780202d95204f2676d2058871fd8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 26 Sep 2018 18:00:40 +0100 Subject: [PATCH 283/472] re-enable test --- spec/unit/matrix-client.spec.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spec/unit/matrix-client.spec.js b/spec/unit/matrix-client.spec.js index c37f99582..a59e0af43 100644 --- a/spec/unit/matrix-client.spec.js +++ b/spec/unit/matrix-client.spec.js @@ -215,8 +215,7 @@ describe("MatrixClient", function() { }); it("should return the same sync state as emitted sync events", async function() { - /* const syncingPromise = new Promise((resolve) => { - throw new Error("fail!!"); + const syncingPromise = new Promise((resolve) => { client.on("sync", function syncListener(state) { expect(state).toEqual(client.getSyncState()); if (state === "SYNCING") { @@ -225,9 +224,8 @@ describe("MatrixClient", function() { } }); }); - */ await client.startClient(); - // await syncingPromise; + await syncingPromise; }); }); From cf6c555e6a875c46a6d1237145e72bfd477354c0 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Sep 2018 11:20:21 +0100 Subject: [PATCH 284/472] Prepare changelog for v0.11.1-rc.1 --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b51feeb9..e42811b5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +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) + + * 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) From 847d40e5674b19e947711cbf50aa75f554f17310 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Sep 2018 11:20:21 +0100 Subject: [PATCH 285/472] v0.11.1-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e59fdc090..6c3728706 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.11.0", + "version": "0.11.1-rc.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From e8e1b431ad6d36b7a986e6ece9deaade63eb13b6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Sep 2018 11:28:01 +0100 Subject: [PATCH 286/472] make usage of hub compatible with latest version (2.5) --- release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.sh b/release.sh index 5a0538a58..ab2eae788 100755 --- a/release.sh +++ b/release.sh @@ -245,7 +245,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" From 056479d45063077c8c25a838615a3594f23f567a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Sep 2018 11:35:52 +0100 Subject: [PATCH 287/472] Revert "v0.11.1-rc.1" This reverts commit 847d40e5674b19e947711cbf50aa75f554f17310. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c3728706..e59fdc090 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.11.1-rc.1", + "version": "0.11.0", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From e8b307dc4f9e79cdc6c3435251dae30a838a01e4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Sep 2018 11:38:25 +0100 Subject: [PATCH 288/472] Prepare changelog for v0.11.1-rc.1 --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e42811b5f..d6f8fbdac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +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.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) From 090c15fe197955c48532de96e04541bb1f38efa4 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Sep 2018 11:38:25 +0100 Subject: [PATCH 289/472] v0.11.1-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e59fdc090..6c3728706 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.11.0", + "version": "0.11.1-rc.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From 19f023e0eeb0d708cb0ff112dcc84813b3f2e337 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Sep 2018 11:42:19 +0100 Subject: [PATCH 290/472] Revert "v0.11.1-rc.1" This reverts commit 090c15fe197955c48532de96e04541bb1f38efa4. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c3728706..e59fdc090 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.11.1-rc.1", + "version": "0.11.0", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From 5e4973a1dc2f62b8c08df4261ee441c0e908cc5b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Sep 2018 11:45:33 +0100 Subject: [PATCH 291/472] changelog --- CHANGELOG.md | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6f8fbdac..b920f350c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,33 +27,6 @@ Changes in [0.11.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta * 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.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) - - * 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) From 634596257d791c91cc3d8b2ed7d50a193837b0b6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Sep 2018 11:47:28 +0100 Subject: [PATCH 292/472] v0.11.1-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e59fdc090..6c3728706 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.11.0", + "version": "0.11.1-rc.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From 1c348f0cdb148cbdc891ae622b178e09a99c18ac Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 27 Sep 2018 18:55:21 +0100 Subject: [PATCH 293/472] disable lazy loading for guests as they cant create filters --- src/client.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client.js b/src/client.js index d11d701d0..b267b8f26 100644 --- a/src/client.js +++ b/src/client.js @@ -3117,6 +3117,9 @@ MatrixClient.prototype.startClient = async function(opts) { // shallow-copy the opts dict before modifying and storing it opts = Object.assign({}, opts); + if (opts.lazyLoadMembers && this.isGuest()) { + opts.lazyLoadMembers = false; + } if (opts.lazyLoadMembers) { const supported = await this.doesServerSupportLazyLoading(); if (supported) { From 83708725b20a772b9d6fadf8b6fb3fc667eca1d5 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Sep 2018 11:16:00 +0200 Subject: [PATCH 294/472] check youre logged in with correct npm user when releasing --- release.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/release.sh b/release.sh index 5a0538a58..06e12b4a3 100755 --- a/release.sh +++ b/release.sh @@ -45,7 +45,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 +61,9 @@ while getopts hc:xz f; do z) skip_jsdoc=1 ;; + u) + expected_npm_user="$OPTARG" + ;; esac done shift `expr $OPTIND - 1` @@ -74,6 +78,12 @@ if [ -z "$skip_changelog" ]; then update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit) fi +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}" From 55ca03f10062fe13f78d7fd4d8a351b1152d654f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 28 Sep 2018 12:51:31 +0200 Subject: [PATCH 295/472] make release compatible with latest release of hub (2.5) --- release.sh | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/release.sh b/release.sh index 06e12b4a3..28eb167ad 100755 --- a/release.sh +++ b/release.sh @@ -11,7 +11,17 @@ 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 USAGE="$0 [-xz] [-c changelog_file] vX.Y.Z" @@ -255,7 +265,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" From fd1b3329f55408eccd257a34e9a06a563a776b44 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 1 Oct 2018 15:23:37 +0200 Subject: [PATCH 296/472] Prepare changelog for v0.11.1 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b920f350c..86c9b82dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +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) From 1e1358fcef92b1388cffadde6983c82fa485dbfe Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 1 Oct 2018 15:23:37 +0200 Subject: [PATCH 297/472] v0.11.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c3728706..b21aae3d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.11.1-rc.1", + "version": "0.11.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From fbca7951fc3457e58b3643ba9ff988fb4a1d422a Mon Sep 17 00:00:00 2001 From: Ben Parsons Date: Mon, 1 Oct 2018 14:29:24 +0100 Subject: [PATCH 298/472] improvements suggested by Bruno --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 15eae610d..20dd640b6 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ See below for how to include libolm to enable end-to-end-encryption. Please chec To start the client: ```javascript -client.startClient({initialSyncLimit: 10}); +await client.startClient({initialSyncLimit: 10}); ``` You can perform a call to `/sync` to get the current state of the client: @@ -76,11 +76,11 @@ client.on("Room.timeline", function(event, room, toStartOfTimeline) { }); ``` -By default, the `matrix-js-sdk` client uses the `MatrixInMemoryStore` to store events as they are received. Access this via `client.store`. For example to iterate through the currently stored timeline for a room: +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.store.rooms[roomId].timeline.forEach(t => { + client.getRoom(roomId).timeline.forEach(t => { console.log(t.event); }); }); From 0a88d419c6fc2143bd51874811ee21c841e9fd53 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 1 Oct 2018 17:21:01 +0200 Subject: [PATCH 299/472] allow non-ff merge from release branch into master --- release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.sh b/release.sh index ab2eae788..0f4ab4086 100755 --- a/release.sh +++ b/release.sh @@ -281,7 +281,7 @@ 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 git push origin master From bd2da08c4eedfd6f90d3ea512692a5eeb45c125e Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Oct 2018 16:48:27 +0100 Subject: [PATCH 300/472] Reject with the actual error on indexeddb error Rather than the event --- src/store/indexeddb-local-backend.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/store/indexeddb-local-backend.js b/src/store/indexeddb-local-backend.js index e0ada7904..e3ae1c1ae 100644 --- a/src/store/indexeddb-local-backend.js +++ b/src/store/indexeddb-local-backend.js @@ -83,7 +83,7 @@ function txnAsPromise(txn) { resolve(event); }; txn.onerror = function(event) { - reject(event); + reject(event.target.error); }; }); } @@ -94,7 +94,7 @@ function reqAsEventPromise(req) { resolve(event); }; req.onerror = function(event) { - reject(event); + reject(event.target.error); }; }); } From 7cd101d8cb06a036b5c67737c47e01249955f621 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 2 Oct 2018 19:22:10 +0100 Subject: [PATCH 301/472] Fix recovery key format --- package.json | 1 + src/client.js | 23 +++++++---------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index db0b14bb1..4ae0b36a7 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "dependencies": { "another-json": "^0.2.0", "babel-runtime": "^6.26.0", + "base58check": "^2.0.0", "bluebird": "^3.5.0", "browser-request": "^0.3.3", "content-type": "^1.0.2", diff --git a/src/client.js b/src/client.js index ceeeff426..951dbcdbb 100644 --- a/src/client.js +++ b/src/client.js @@ -49,6 +49,7 @@ import {InvalidStoreError} from './errors'; import Crypto from './crypto'; import { isCryptoAvailable } from './crypto'; +import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey'; const LAZY_LOADING_MESSAGES_FILTER = { lazy_load_members: true, @@ -882,9 +883,7 @@ MatrixClient.prototype.prepareKeyBackupVersion = function() { auth_data: { public_key: publicKey, }, - // FIXME: pickle isn't the right thing to use, but we don't have - // anything else yet, so use it for now - recovery_key: decryption.pickle("secret_key"), + recovery_key: encodeRecoveryKey(decryption.get_private_key()), }; } finally { decryption.free(); @@ -991,26 +990,17 @@ MatrixClient.prototype.backupAllGroupSessions = function(version) { return this._crypto.backupAllGroupSessions(version); }; -MatrixClient.prototype.isValidRecoveryKey = function(decryptionKey) { - if (this._crypto === null) { - throw new Error("End-to-end encryption disabled"); - } - - const decryption = new global.Olm.PkDecryption(); +MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) { try { - // FIXME: see the FIXME in createKeyBackupVersion - decryption.unpickle("secret_key", decryptionKey); + decodeRecoveryKey(recoveryKey); return true; } catch (e) { - console.log(e); return false; - } finally { - decryption.free(); } }; MatrixClient.prototype.restoreKeyBackups = function( - decryptionKey, targetRoomId, targetSessionId, version, + recoveryKey, targetRoomId, targetSessionId, version, ) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); @@ -1021,9 +1011,10 @@ MatrixClient.prototype.restoreKeyBackups = function( const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version); // FIXME: see the FIXME in createKeyBackupVersion + const privkey = decodeRecoveryKey(recoveryKey); const decryption = new global.Olm.PkDecryption(); try { - decryption.unpickle("secret_key", decryptionKey); + decryption.init_with_private_key(privkey); } catch(e) { decryption.free(); throw e; From 262ace1773f22bb917078e74fb72c6e1dabe38da Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 3 Oct 2018 10:20:57 +0100 Subject: [PATCH 302/472] commit the recovery key util file --- src/crypto/recoverykey.js | 44 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/crypto/recoverykey.js diff --git a/src/crypto/recoverykey.js b/src/crypto/recoverykey.js new file mode 100644 index 000000000..82de3a158 --- /dev/null +++ b/src/crypto/recoverykey.js @@ -0,0 +1,44 @@ +/* +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 base58check from 'base58check'; + +// picked arbitrarily but to try & avoid clashing with any bitcoin ones +const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01]; + +export function encodeRecoveryKey(key) { + const base58key = base58check.encode(Buffer.from(OLM_RECOVERY_KEY_PREFIX), Buffer.from(key)); + return base58key.match(/.{1,4}/g).join(" "); +} + +export function decodeRecoveryKey(recoverykey) { + const result = base58check.decode(recoverykey.replace(/ /, '')); + // the encoding doesn't include the length of the prefix, so the + // decoder assumes it's 1 byte. sigh. + const prefix = Buffer.concat([result.prefix, result.data.slice(0, OLM_RECOVERY_KEY_PREFIX.length - 1)]); + + if (!prefix.equals(Buffer.from(OLM_RECOVERY_KEY_PREFIX))) { + throw new Error("Incorrect prefix"); + } + + const key = result.data.slice(OLM_RECOVERY_KEY_PREFIX.length - 1); + + if (key.length !== global.Olm.PRIVATE_KEY_LENGTH) { + throw new Error("Incorrect length"); + } + + return key; +} From 35d584c67bddf0b7337ea1438888de12b7a1e74f Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 Oct 2018 13:05:45 +0100 Subject: [PATCH 303/472] Remove outdated comment --- src/crypto/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 9ed3c7113..1e0e18d13 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -128,8 +128,6 @@ utils.inherits(Crypto, EventEmitter); * Returns a promise which resolves once the crypto module is ready for use. */ Crypto.prototype.init = async function() { - // Olm is just an object with a .then, not a fully-fledged promise, so - // pass it into bluebird to make it a proper promise. await global.Olm.init(); const sessionStoreHasAccount = Boolean(this._sessionStore.getEndToEndAccount()); From 8116c5b3f74062ae64ac753cb6f785c025304f8d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 Oct 2018 13:49:32 +0100 Subject: [PATCH 304/472] 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). --- src/crypto/store/indexeddb-crypto-store.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 249b29b63..012ae4800 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -92,6 +92,15 @@ export default class IndexedDBCryptoStore { console.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( `unable to connect to indexeddb ${this._dbName}` + From 4cda54ca1ca812662382e1cfe31bbac38d419cbf Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 Oct 2018 15:15:30 +0100 Subject: [PATCH 305/472] lint --- src/crypto/store/indexeddb-crypto-store.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 012ae4800..bb2a22ca7 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -96,11 +96,13 @@ export default class IndexedDBCryptoStore { // 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; - }); + return backend.doTxn( + 'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => { + backend.getEndToEndInboundGroupSession('', '', txn, () => {}); + }).then(() => { + return backend; + }, + ); }).catch((e) => { console.warn( `unable to connect to indexeddb ${this._dbName}` + From 40dc13b2e2b276570551d3025ea81e3884647e85 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 4 Oct 2018 15:38:08 +0100 Subject: [PATCH 306/472] lint try 2 --- src/crypto/store/indexeddb-crypto-store.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index bb2a22ca7..0e0654deb 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -97,7 +97,9 @@ export default class IndexedDBCryptoStore { // 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) => { + 'readonly', + [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], + (txn) => { backend.getEndToEndInboundGroupSession('', '', txn, () => {}); }).then(() => { return backend; From 258adda67c0b7d43a053249f564260e10de8908d Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 4 Oct 2018 15:19:20 -0400 Subject: [PATCH 307/472] retry key backups when they fail --- spec/unit/crypto/backup.spec.js | 192 +++++++++++++++++- src/client.js | 2 + src/crypto/index.js | 126 ++++++------ .../store/indexeddb-crypto-store-backend.js | 70 +++++++ src/crypto/store/indexeddb-crypto-store.js | 33 +++ src/crypto/store/memory-crypto-store.js | 35 ++++ 6 files changed, 399 insertions(+), 59 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 25f6c112a..7a7f0b414 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -98,7 +98,7 @@ describe("MegolmBackup", function() { mockCrypto = testUtils.mock(Crypto, 'Crypto'); mockCrypto.backupKey = new global.Olm.PkEncryption(); mockCrypto.backupKey.set_recipient_key( - "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK", + "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", ); mockCrypto.backupInfo = { version: 1, @@ -134,7 +134,7 @@ describe("MegolmBackup", function() { megolmDecryption.olmlib = mockOlmLib; }); - it('automatically backs up keys', function() { + it('automatically calls the key back up', function() { const groupSession = new global.Olm.OutboundGroupSession(); groupSession.create(); @@ -169,6 +169,194 @@ describe("MegolmBackup", function() { expect(mockCrypto.backupGroupSession).toHaveBeenCalled(); }); }); + + it('sends backups to the server', function () { + const groupSession = new global.Olm.OutboundGroupSession(); + groupSession.create(); + const ibGroupSession = new global.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/Og0mOBr066SpjqqbTmoK" + }, + }); + let numCalls = 0; + return new Promise((resolve, reject) => { + client._http.authedRequest = function(callback, method, path, queryParams, data, opts) { + expect(++numCalls <= 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 global.Olm.OutboundGroupSession(); + groupSession.create(); + const ibGroupSession = new global.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/Og0mOBr066SpjqqbTmoK" + }, + }); + let numCalls = 0; + return new Promise((resolve, reject) => { + client._http.authedRequest = function(callback, method, path, queryParams, data, opts) { + expect(++numCalls <= 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() { diff --git a/src/client.js b/src/client.js index c864bf490..7080fbcf6 100644 --- a/src/client.js +++ b/src/client.js @@ -848,6 +848,8 @@ MatrixClient.prototype.enableKeyBackup = function(info) { this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); this.emit('keyBackupStatus', true); + + this._crypto._maybeSendKeyBackup(); }; /** diff --git a/src/crypto/index.js b/src/crypto/index.js index d45797fcb..41dfdff73 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -83,6 +83,7 @@ function Crypto(baseApis, sessionStore, userId, deviceId, this.backupInfo = null; // The info dict from /room_keys/version this.backupKey = null; // The encryption key object this._checkedForBackup = false; // Have we checked the server for a backup we can use? + this._sendingBackups = false; // Are we currently sending backups? this._olmDevice = new OlmDevice(sessionStore, cryptoStore); this._deviceList = new DeviceList( @@ -965,51 +966,62 @@ Crypto.prototype.importRoomKeys = function(keys) { ); }; -Crypto.prototype._backupPayloadForSession = function( - senderKey, forwardingCurve25519KeyChain, - sessionId, sessionKey, keysClaimed, - exportFormat, -) { - // new session. - const session = new Olm.InboundGroupSession(); - try { - if (exportFormat) { - session.import_session(sessionKey); - } else { - session.create(sessionKey); - } - if (sessionId != session.session_id()) { - throw new Error( - "Mismatched group session ID from senderKey: " + - senderKey, - ); - } +Crypto.prototype._maybeSendKeyBackup = async function() { + if (!this._sendingBackups) { + this._sendingBackups = true; + while (1) { + if (!this.backupKey) { + this._sendingBackups = false; + return; + } + // FIXME: figure out what limit is reasonable + const sessions = await this._cryptoStore.getSessionsNeedingBackup(10); + if (!sessions.length) { + this._sendingBackups = false; + return; + } + const data = {}; + for (const session of sessions) { + const room_id = session.sessionData.room_id; + if (data[room_id] === undefined) + data[room_id] = {sessions: {}}; - if (!exportFormat) { - sessionKey = session.export_session(); - } - const firstKnownIndex = session.first_known_index(); + const sessionData = await this._olmDevice.exportInboundGroupSession(session.senderKey, session.sessionId, session.sessionData); + sessionData.algorithm = olmlib.MEGOLM_ALGORITHM; + delete sessionData.session_id; + delete sessionData.room_id; + const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); - const sessionData = { - algorithm: olmlib.MEGOLM_ALGORITHM, - sender_key: senderKey, - sender_claimed_keys: keysClaimed, - session_key: sessionKey, - forwarding_curve25519_key_chain: forwardingCurve25519KeyChain, - }; - const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); - return { - first_message_index: firstKnownIndex, - forwarded_count: forwardingCurve25519KeyChain.length, - is_verified: false, // FIXME: how do we determine this? - session_data: encrypted, - }; - } finally { - session.free(); + data[room_id]['sessions'][session.sessionId] = { + first_message_index: 1, // FIXME + forwarded_count: (sessionData.forwardingCurve25519KeyChain || []).length, + is_verified: false, // FIXME: how do we determine this? + session_data: encrypted, + }; + } + + let successful = false; + do { + if (!this.backupKey) { + this._sendingBackups = false; + return; + } + try { + await this._baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, {rooms: data}); + successful = true; + await this._cryptoStore.unmarkSessionsNeedingBackup(sessions); + } + catch (e) { + console.log("send failed", e); + // FIXME: pause + } + } while (!successful); + // FIXME: pause between iterations? + } } -}; +} -Crypto.prototype.backupGroupSession = function( +Crypto.prototype.backupGroupSession = async function( roomId, senderKey, forwardingCurve25519KeyChain, sessionId, sessionKey, keysClaimed, exportFormat, @@ -1018,26 +1030,26 @@ Crypto.prototype.backupGroupSession = function( throw new Error("Key backups are not enabled"); } - const data = this._backupPayloadForSession( - senderKey, forwardingCurve25519KeyChain, - sessionId, sessionKey, keysClaimed, - exportFormat, - ); - return this._baseApis.sendKeyBackup(roomId, sessionId, this.backupInfo.version, data); + await this._cryptoStore.markSessionsNeedingBackup([{ + senderKey: senderKey, + sessionId: sessionId, + }]); + + this._maybeSendKeyBackup(); }; Crypto.prototype.backupAllGroupSessions = async function(version) { - const keys = await this.exportRoomKeys(); - const data = {}; - for (const key of keys) { - if (data[key.room_id] === undefined) data[key.room_id] = {sessions: {}}; + await this._cryptoStore.doTxn( + 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS, IndexedDBCryptoStore.STORE_BACKUP], (txn) => { + this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => { + if (session !== null) { + this._cryptoStore.markSessionsNeedingBackup([session], txn); + } + }); + } + ); - data[key.room_id]['sessions'][key.session_id] = this._backupPayloadForSession( - key.sender_key, key.forwarding_curve25519_key_chain, - key.session_id, key.session_key, key.sender_claimed_keys, true, - ); - } - return this._baseApis.sendKeyBackup(undefined, undefined, version, {rooms: data}); + this._maybeSendKeyBackup(); }; /* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307 diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index 4a7f48789..9935dbd38 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -460,6 +460,71 @@ 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 + }); + } + //sessions.push(cursor.value); + if (!limit || sessions.length < limit) { + cursor.continue(); + } + } + } + }); + } + + unmarkSessionsNeedingBackup(sessions) { + const 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) => { + console.log(session); + 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); @@ -498,6 +563,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. } diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 249b29b63..0eca4c373 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -407,6 +407,38 @@ export default class IndexedDBCryptoStore { this._backendPromise.value().getEndToEndRooms(txn, func); } + /** + * 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. + */ + getSessionsNeedingBackup(limit) { + return this._connect().then((backend) => { + return backend.getSessionsNeedingBackup(limit); + }); + } + + /** + * Unmark sessions as needing to be backed up. + * @param {[object]} The sessions that need to be backed up. + */ + unmarkSessionsNeedingBackup(sessions) { + return this._connect().then((backend) => { + return backend.unmarkSessionsNeedingBackup(sessions); + }); + } + + /** + * Mark sessions as needing to be backed up. + * @param {[object]} The sessions that need to be backed up. + * @param {*} txn An active transaction. See doTxn(). (optional) + */ + 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 +472,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'; diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 469cdb49b..cd5ec5be4 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -41,6 +41,8 @@ export default class MemoryCryptoStore { this._deviceData = null; // roomId -> Opaque roomInfo object this._rooms = {}; + // Set of {senderCurve25519Key+'/'+sessionId} + this._sessionsNeedingBackup = {}; } /** @@ -295,6 +297,39 @@ 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); + } + + unmarkSessionsNeedingBackup(sessions) { + for(const session of sessions) { + delete this._sessionsNeedingBackup[session.senderKey + '/' + session.sessionId]; + } + return Promise.resolve(); + } + + markSessionsNeedingBackup(sessions) { + for(const session of sessions) { + this._sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true; + } + return Promise.resolve(); + } + + // Session key backups + doTxn(mode, stores, func) { return Promise.resolve(func(null)); } From d9fe194111984eced513f0e6a54a82772d0fe006 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 4 Oct 2018 13:42:07 -0600 Subject: [PATCH 308/472] 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. --- src/models/room.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/models/room.js b/src/models/room.js index c99d87ba5..4b685c878 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -198,7 +198,10 @@ utils.inherits(Room, EventEmitter); */ Room.prototype.getVersion = function() { const createEvent = this.currentState.getStateEvents("m.room.create", ""); - if (!createEvent) return null; + 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; From caba350b33973aedc041b59ef172c677456d22f8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 5 Oct 2018 11:52:55 +0200 Subject: [PATCH 309/472] throw error with same name and message over idb worker boundary instead of string currently thrown. This allows handling error from the main thread. --- src/store/indexeddb-remote-backend.js | 4 +++- src/store/indexeddb-store-worker.js | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/store/indexeddb-remote-backend.js b/src/store/indexeddb-remote-backend.js index 07ee18516..035a821ff 100644 --- a/src/store/indexeddb-remote-backend.js +++ b/src/store/indexeddb-remote-backend.js @@ -184,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); diff --git a/src/store/indexeddb-store-worker.js b/src/store/indexeddb-store-worker.js index bf2c1184d..4e66f51aa 100644 --- a/src/store/indexeddb-store-worker.js +++ b/src/store/indexeddb-store-worker.js @@ -135,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, + }, }); }); } From 264b20535e8759314647d60b881299006679978f Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 5 Oct 2018 12:13:05 +0100 Subject: [PATCH 310/472] Silence bluebird warnings --- src/client.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client.js b/src/client.js index d5923ee0e..577cf5ba8 100644 --- a/src/client.js +++ b/src/client.js @@ -46,6 +46,9 @@ import ReEmitter from './ReEmitter'; import RoomList from './crypto/RoomList'; import {InvalidStoreError} from './errors'; +// Disable warnings for now: we use deprecated bluebird functions +// and need to migrate, but they spam the console with warnings. +Promise.config({warnings: false}); const LAZY_LOADING_MESSAGES_FILTER = { lazy_load_members: true, From 59e60665795ecbd3684d1ada12ced5c9341c7f11 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 9 Oct 2018 14:15:03 +0100 Subject: [PATCH 311/472] 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. --- package.json | 2 +- src/crypto/recoverykey.js | 42 +++++++++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 4ae0b36a7..17c3ec5ce 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,9 @@ "dependencies": { "another-json": "^0.2.0", "babel-runtime": "^6.26.0", - "base58check": "^2.0.0", "bluebird": "^3.5.0", "browser-request": "^0.3.3", + "bs58": "^4.0.1", "content-type": "^1.0.2", "request": "^2.53.0" }, diff --git a/src/crypto/recoverykey.js b/src/crypto/recoverykey.js index 82de3a158..69dc8ae6e 100644 --- a/src/crypto/recoverykey.js +++ b/src/crypto/recoverykey.js @@ -14,31 +14,51 @@ See the License for the specific language governing permissions and limitations under the License. */ -import base58check from 'base58check'; +import bs58 from 'bs58'; // picked arbitrarily but to try & avoid clashing with any bitcoin ones +// (also base58 encoded, albeit with a lot of hashing) const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01]; export function encodeRecoveryKey(key) { - const base58key = base58check.encode(Buffer.from(OLM_RECOVERY_KEY_PREFIX), Buffer.from(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 = base58check.decode(recoverykey.replace(/ /, '')); - // the encoding doesn't include the length of the prefix, so the - // decoder assumes it's 1 byte. sigh. - const prefix = Buffer.concat([result.prefix, result.data.slice(0, OLM_RECOVERY_KEY_PREFIX.length - 1)]); + const result = bs58.decode(recoverykey.replace(/ /g, '')); - if (!prefix.equals(Buffer.from(OLM_RECOVERY_KEY_PREFIX))) { - throw new Error("Incorrect prefix"); + let parity = 0; + for (const b of result) { + parity ^= b; + } + if (parity !== 0) { + throw new Error("Incorrect parity"); } - const key = result.data.slice(OLM_RECOVERY_KEY_PREFIX.length - 1); + 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 (key.length !== global.Olm.PRIVATE_KEY_LENGTH) { + if (result.length !== OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1) { throw new Error("Incorrect length"); } - return key; + return result.slice( + OLM_RECOVERY_KEY_PREFIX.length, + OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH, + ); } From ada4b6ef16a3557e24b54e494d5b5a6f644c64f3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 9 Oct 2018 15:46:12 +0100 Subject: [PATCH 312/472] Lint --- spec/unit/crypto/backup.spec.js | 9 +++++---- src/client.js | 4 +++- src/crypto/index.js | 2 +- src/crypto/recoverykey.js | 5 ++++- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 25f6c112a..f1da584f4 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -43,13 +43,14 @@ 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: 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', + session_id: SESSION_ID, ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N' + 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl' + 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs', @@ -222,7 +223,7 @@ describe("MegolmBackup", function() { "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", ROOM_ID, - 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', + SESSION_ID, ).then(() => { return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); }).then((res) => { @@ -236,10 +237,10 @@ describe("MegolmBackup", function() { rooms: { [ROOM_ID]: { sessions: { - 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc': KEY_BACKUP_DATA, + SESSION_ID: KEY_BACKUP_DATA, }, }, - } + }, }); }; return client.restoreKeyBackups( diff --git a/src/client.js b/src/client.js index 5a9481122..9c3a86911 100644 --- a/src/client.js +++ b/src/client.js @@ -1032,7 +1032,9 @@ MatrixClient.prototype.restoreKeyBackups = function( if (!roomData.sessions) continue; totalKeyCount += Object.keys(roomData.sessions).length; - const roomKeys = keysFromRecoverySession(roomData.sessions, decryption, roomId, roomKeys); + const roomKeys = keysFromRecoverySession( + roomData.sessions, decryption, roomId, roomKeys, + ); for (const k of roomKeys) { k.room_id = roomId; keys.push(k); diff --git a/src/crypto/index.js b/src/crypto/index.js index 7aa76e37e..b6926b837 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -974,7 +974,7 @@ Crypto.prototype._backupPayloadForSession = function( exportFormat, ) { // new session. - const session = new Olm.InboundGroupSession(); + const session = new global.Olm.InboundGroupSession(); try { if (exportFormat) { session.import_session(sessionKey); diff --git a/src/crypto/recoverykey.js b/src/crypto/recoverykey.js index 69dc8ae6e..bb85697e8 100644 --- a/src/crypto/recoverykey.js +++ b/src/crypto/recoverykey.js @@ -53,7 +53,10 @@ export function decodeRecoveryKey(recoverykey) { } } - if (result.length !== OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1) { + if ( + result.length !== + OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1 + ) { throw new Error("Incorrect length"); } From 30362091e543b09f713d77603ce8c77edd5f47f9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 10 Oct 2018 16:59:36 +0100 Subject: [PATCH 313/472] 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 --- src/client.js | 63 +++--------------------------------------- src/errors.js | 2 +- src/filter.js | 11 ++++++++ src/sync.js | 76 +++++++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 90 insertions(+), 62 deletions(-) diff --git a/src/client.js b/src/client.js index 577cf5ba8..d5ca91059 100644 --- a/src/client.js +++ b/src/client.js @@ -44,22 +44,11 @@ const ContentHelpers = require("./content-helpers"); import ReEmitter from './ReEmitter'; import RoomList from './crypto/RoomList'; -import {InvalidStoreError} from './errors'; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. Promise.config({warnings: false}); -const LAZY_LOADING_MESSAGES_FILTER = { - lazy_load_members: true, -}; - -const LAZY_LOADING_SYNC_FILTER = { - room: { - state: LAZY_LOADING_MESSAGES_FILTER, - }, -}; - const SCROLLBACK_DELAY_MS = 3000; let CRYPTO_ENABLED = false; @@ -2071,7 +2060,7 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) { let params = undefined; if (this._clientOpts.lazyLoadMembers) { - params = {filter: JSON.stringify(LAZY_LOADING_MESSAGES_FILTER)}; + params = {filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER)}; } // TODO: we should implement a backoff (as per scrollback()) to deal more @@ -2152,7 +2141,7 @@ function(roomId, fromToken, limit, dir, timelineFilter = undefined) { if (this._clientOpts.lazyLoadMembers) { // create a shallow copy of LAZY_LOADING_MESSAGES_FILTER, // so the timelineFilter doesn't get written into it below - filter = Object.assign({}, LAZY_LOADING_MESSAGES_FILTER); + filter = Object.assign({}, Filter.LAZY_LOADING_MESSAGES_FILTER); } if (timelineFilter) { // XXX: it's horrific that /messages' filter parameter doesn't match @@ -3109,29 +3098,6 @@ MatrixClient.prototype.startClient = async function(opts) { // shallow-copy the opts dict before modifying and storing it opts = Object.assign({}, opts); - if (opts.lazyLoadMembers && this.isGuest()) { - opts.lazyLoadMembers = false; - } - if (opts.lazyLoadMembers) { - const supported = await this.doesServerSupportLazyLoading(); - if (supported) { - opts.filter = await this.createFilter(LAZY_LOADING_SYNC_FILTER); - } else { - console.log("LL: lazy loading requested but not supported " + - "by server, so disabling"); - opts.lazyLoadMembers = false; - } - } - // need to vape the store when enabling LL and wasn't enabled before - const shouldClear = await this._wasLazyLoadingToggled(opts.lazyLoadMembers); - if (shouldClear) { - const reason = InvalidStoreError.TOGGLED_LAZY_LOADING; - throw new InvalidStoreError(reason, !!opts.lazyLoadMembers); - } - if (opts.lazyLoadMembers && this._crypto) { - this._crypto.enableLazyLoading(); - } - opts.crypto = this._crypto; opts.canResetEntireTimeline = (roomId) => { if (!this._canResetTimelineCallback) { @@ -3140,40 +3106,19 @@ MatrixClient.prototype.startClient = async function(opts) { return this._canResetTimelineCallback(roomId); }; this._clientOpts = opts; - await this._storeClientOptions(this._clientOpts); this._syncApi = new SyncApi(this, opts); this._syncApi.sync(); }; -/** - * 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 */ -MatrixClient.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.store.isNewlyCreated(); - if (!isStoreNewlyCreated) { - const prevClientOptions = await this.store.getClientOptions(); - if (prevClientOptions) { - lazyLoadMembersBefore = !!prevClientOptions.lazyLoadMembers; - } - return lazyLoadMembersBefore !== lazyLoadMembers; - } - return false; -}; - /** * store client options with boolean/string/numeric values * to know in the next session what flags the sync data was * created with (e.g. lazy loading) * @param {object} opts the complete set of client options * @return {Promise} for store operation */ -MatrixClient.prototype._storeClientOptions = function(opts) { +MatrixClient.prototype._storeClientOptions = function() { const primTypes = ["boolean", "string", "number"]; - const serializableOpts = Object.entries(opts) + const serializableOpts = Object.entries(this._clientOpts) .filter(([key, value]) => { return primTypes.includes(typeof value); }) diff --git a/src/errors.js b/src/errors.js index 04e14f2c8..337b058db 100644 --- a/src/errors.js +++ b/src/errors.js @@ -2,7 +2,7 @@ // because of http://babeljs.io/docs/usage/caveats/#classes function InvalidStoreError(reason, value) { const message = `Store is invalid because ${reason}, ` + - `please delete all data and retry`; + `please stopthe client, delete all data and start the client again`; const instance = Reflect.construct(Error, [message]); Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this)); instance.reason = reason; diff --git a/src/filter.js b/src/filter.js index e0f03daa7..a63fcee13 100644 --- a/src/filter.js +++ b/src/filter.js @@ -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 diff --git a/src/sync.js b/src/sync.js index 39e82abe4..634d07e1e 100644 --- a/src/sync.js +++ b/src/sync.js @@ -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 @@ -100,6 +102,7 @@ function SyncApi(client, opts) { 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(), @@ -422,6 +425,28 @@ 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(); + console.log("store newly created? "+isStoreNewlyCreated); + if (!isStoreNewlyCreated) { + const prevClientOptions = await this.client.store.getClientOptions(); + if (prevClientOptions) { + lazyLoadMembersBefore = !!prevClientOptions.lazyLoadMembers; + } + console.log("prev ll: "+lazyLoadMembersBefore); + return lazyLoadMembersBefore !== lazyLoadMembers; + } + return false; +}; + /** * Main entry point */ @@ -444,6 +469,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 { @@ -458,9 +485,49 @@ 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(); + console.log("server supports ll? "+supported); + 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); + console.log("was toggled? "+shouldClear); + 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._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) { @@ -588,7 +655,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); + } }; /** From dea70af889767cc1cb5e8958561791007925bd79 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 10 Oct 2018 17:06:26 +0100 Subject: [PATCH 314/472] remove debug logging --- src/sync.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/sync.js b/src/sync.js index 634d07e1e..76122874b 100644 --- a/src/sync.js +++ b/src/sync.js @@ -435,13 +435,11 @@ SyncApi.prototype._wasLazyLoadingToggled = async function(lazyLoadMembers) { // if we don't know any better let lazyLoadMembersBefore = false; const isStoreNewlyCreated = await this.client.store.isNewlyCreated(); - console.log("store newly created? "+isStoreNewlyCreated); if (!isStoreNewlyCreated) { const prevClientOptions = await this.client.store.getClientOptions(); if (prevClientOptions) { lazyLoadMembersBefore = !!prevClientOptions.lazyLoadMembers; } - console.log("prev ll: "+lazyLoadMembersBefore); return lazyLoadMembersBefore !== lazyLoadMembers; } return false; @@ -494,7 +492,6 @@ SyncApi.prototype.sync = function() { } if (this.opts.lazyLoadMembers) { const supported = await client.doesServerSupportLazyLoading(); - console.log("server supports ll? "+supported); if (supported) { this.opts.filter = await client.createFilter( Filter.LAZY_LOADING_SYNC_FILTER, @@ -507,7 +504,6 @@ SyncApi.prototype.sync = function() { } // need to vape the store when enabling LL and wasn't enabled before const shouldClear = await this._wasLazyLoadingToggled(this.opts.lazyLoadMembers); - console.log("was toggled? "+shouldClear); if (shouldClear) { this._storeIsInvalid = true; const reason = InvalidStoreError.TOGGLED_LAZY_LOADING; From ecc3e18e850929431f8c966520eb2881df1be6b5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 10 Oct 2018 17:27:06 +0100 Subject: [PATCH 315/472] typo --- src/errors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/errors.js b/src/errors.js index 337b058db..409fafd9c 100644 --- a/src/errors.js +++ b/src/errors.js @@ -2,7 +2,7 @@ // because of http://babeljs.io/docs/usage/caveats/#classes function InvalidStoreError(reason, value) { const message = `Store is invalid because ${reason}, ` + - `please stopthe client, delete all data and start the client again`; + `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; From da65f43983af1e5504ef0b1ba2d4a38c6d9acc81 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 10 Oct 2018 19:31:28 -0400 Subject: [PATCH 316/472] wrap backup sending in a try, and add delays --- spec/unit/crypto/backup.spec.js | 2 + src/crypto/index.js | 94 +++++++++++++++++++-------------- 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 7a7f0b414..de910592b 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -171,6 +171,7 @@ describe("MegolmBackup", function() { }); it('sends backups to the server', function () { + this.timeout(12000); const groupSession = new global.Olm.OutboundGroupSession(); groupSession.create(); const ibGroupSession = new global.Olm.InboundGroupSession(); @@ -263,6 +264,7 @@ describe("MegolmBackup", function() { }); it('retries when a backup fails', function () { + this.timeout(12000); const groupSession = new global.Olm.OutboundGroupSession(); groupSession.create(); const ibGroupSession = new global.Olm.InboundGroupSession(); diff --git a/src/crypto/index.js b/src/crypto/index.js index 41dfdff73..7104320ee 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -969,54 +969,68 @@ Crypto.prototype.importRoomKeys = function(keys) { Crypto.prototype._maybeSendKeyBackup = async function() { if (!this._sendingBackups) { this._sendingBackups = true; - while (1) { - if (!this.backupKey) { - this._sendingBackups = false; - return; - } - // FIXME: figure out what limit is reasonable - const sessions = await this._cryptoStore.getSessionsNeedingBackup(10); - if (!sessions.length) { - this._sendingBackups = false; - return; - } - const data = {}; - for (const session of sessions) { - const room_id = session.sessionData.room_id; - if (data[room_id] === undefined) - data[room_id] = {sessions: {}}; - - const sessionData = await this._olmDevice.exportInboundGroupSession(session.senderKey, session.sessionId, session.sessionData); - sessionData.algorithm = olmlib.MEGOLM_ALGORITHM; - delete sessionData.session_id; - delete sessionData.room_id; - const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); - - data[room_id]['sessions'][session.sessionId] = { - first_message_index: 1, // FIXME - forwarded_count: (sessionData.forwardingCurve25519KeyChain || []).length, - is_verified: false, // FIXME: how do we determine this? - session_data: encrypted, - }; - } - - let successful = false; - do { + try { + // wait between 0 and 10 seconds, to avoid backup requests from + // different clients hitting the server all at the same time when a + // new key is sent + await new Promise((resolve, reject) => { + setTimeout(resolve, Math.random() * 10000); + }); + let numFailures = 0; // number of consecutive failures + while (1) { if (!this.backupKey) { - this._sendingBackups = false; return; } + // FIXME: figure out what limit is reasonable + const sessions = await this._cryptoStore.getSessionsNeedingBackup(10); + if (!sessions.length) { + return; + } + const data = {}; + for (const session of sessions) { + const room_id = session.sessionData.room_id; + if (data[room_id] === undefined) + data[room_id] = {sessions: {}}; + + const sessionData = await this._olmDevice.exportInboundGroupSession(session.senderKey, session.sessionId, session.sessionData); + sessionData.algorithm = olmlib.MEGOLM_ALGORITHM; + delete sessionData.session_id; + delete sessionData.room_id; + const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); + + data[room_id]['sessions'][session.sessionId] = { + first_message_index: 1, // FIXME + forwarded_count: (sessionData.forwardingCurve25519KeyChain || []).length, + is_verified: false, // FIXME: how do we determine this? + session_data: encrypted, + }; + } + try { await this._baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, {rooms: data}); - successful = true; + numFailures = 0; await this._cryptoStore.unmarkSessionsNeedingBackup(sessions); } - catch (e) { - console.log("send failed", e); - // FIXME: pause + catch (err) { + numFailures++; + console.log("send failed", err); + if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) { + // retrying probably won't help much, so we should give up + // FIXME: disable backups? + return; + } } - } while (!successful); - // FIXME: pause between iterations? + if (numFailures) { + // exponential backoff if we have failures + await new Promise((resolve, reject) => { + setTimeout(resolve, 1000 * Math.pow(2, Math.min(numFailures - 1, 4))); + }); + } + } + } + finally + { + this._sendingBackups = false; } } } From fc59bc2992d9543be987aaac60a22aa567aa81eb Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Wed, 10 Oct 2018 19:32:07 -0400 Subject: [PATCH 317/472] add localstorage support for key backups --- src/crypto/store/localStorage-crypto-store.js | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index 3f2f0d09a..71a904fd8 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -32,6 +32,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; @@ -165,6 +166,48 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { func(result); } + getSessionsNeedingBackup(limit) { + const sessions = []; + + for (const session in getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP)) { + const senderKey = session.substr(0, 43); + const sessionId = session.substr(44); + getEndToEndInboundGroupSession(senderKey, sessionId, null, (sessionData) => { + sessions.push({ + senderKey: senderKey, + sessionId: sessionId, + sessionData: sessionData, + }); + }) + if (limit && session.length >= limit) { + break; + } + } + return Promise.resolve(sessions); + } + + 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_SESSION_NEEDING_BACKUP, sessionsNeedinBackup, + ); + 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_SESSION_NEEDING_BACKUP, sessionsNeedinBackup, + ); + return Promise.resolve(); + } + /** * Delete all data from this store. * From 3cf23f8a5ca23227aa566f4ff5280cfc22e94d11 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 11 Oct 2018 12:13:25 +0100 Subject: [PATCH 318/472] Document breaking change --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86c9b82dc..896e3667b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +Latest Changes: +=============== + +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. + 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) From d098b3902417d99a5672dc64ebcbc26170e2f7bc Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 11 Oct 2018 14:11:40 +0200 Subject: [PATCH 319/472] never replace /sync'ed memberships with OOB ones --- src/models/room-state.js | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/models/room-state.js b/src/models/room-state.js index 52bc5ed36..a868d63c1 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -483,21 +483,9 @@ RoomState.prototype._setOutOfBandMember = function(stateEvent) { } const userId = stateEvent.getStateKey(); const existingMember = this.getMember(userId); - if (existingMember) { - const existingMemberEvent = existingMember.events.member; - // ignore out of band members with events we are - // already aware of. - if (existingMemberEvent.getId() === stateEvent.getId()) { - return; - } - // this member was updated since we started - // loading the out of band members. - // Ignore the out of band member and clear - // the "supersedes" flag as the out of members are now loaded - if (existingMember.supersedesOutOfBand()) { - existingMember.clearSupersedesOutOfBand(); - return; - } + // never replace members replaced as part of the sync + if (existingMember && !existingMember.isOutOfBand()) { + return; } const member = this._getOrCreateMember(userId, stateEvent); From a0ef6ab811d82a4a81e1fac34293745027ed55e6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 11 Oct 2018 14:14:42 +0200 Subject: [PATCH 320/472] typo --- src/models/room-state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room-state.js b/src/models/room-state.js index a868d63c1..9a85e71ce 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -483,7 +483,7 @@ RoomState.prototype._setOutOfBandMember = function(stateEvent) { } const userId = stateEvent.getStateKey(); const existingMember = this.getMember(userId); - // never replace members replaced as part of the sync + // never replace members received as part of the sync if (existingMember && !existingMember.isOutOfBand()) { return; } From 2ed694b041438e9f4ec1f0789837551cb4bd14d6 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 11 Oct 2018 14:32:03 +0200 Subject: [PATCH 321/472] remove supersedes OOB logic --- spec/unit/room-member.spec.js | 16 --------- spec/unit/room-state.spec.js | 62 +++-------------------------------- src/models/room-member.js | 26 --------------- src/models/room-state.js | 24 ++------------ 4 files changed, 7 insertions(+), 121 deletions(-) diff --git a/spec/unit/room-member.spec.js b/spec/unit/room-member.spec.js index 5212282b1..298771128 100644 --- a/spec/unit/room-member.spec.js +++ b/spec/unit/room-member.spec.js @@ -201,22 +201,6 @@ describe("RoomMember", function() { }); }); - describe("supersedesOutOfBand", function() { - it("should be set by markSupersedesOutOfBand", function() { - const member = new RoomMember(); - expect(member.supersedesOutOfBand()).toEqual(false); - member.markSupersedesOutOfBand(); - expect(member.supersedesOutOfBand()).toEqual(true); - }); - it("should be cleared by clearSupersedesOutOfBand", function() { - const member = new RoomMember(); - member.markSupersedesOutOfBand(); - expect(member.supersedesOutOfBand()).toEqual(true); - member.clearSupersedesOutOfBand(); - expect(member.supersedesOutOfBand()).toEqual(false); - }); - }); - describe("setMembershipEvent", function() { const joinEvent = utils.mkMembership({ event: true, diff --git a/spec/unit/room-state.spec.js b/spec/unit/room-state.spec.js index 6619c64a7..baa602781 100644 --- a/spec/unit/room-state.spec.js +++ b/spec/unit/room-state.spec.js @@ -299,41 +299,25 @@ describe("RoomState", function() { expect(eventReceived).toEqual(true); }); - it("should overwrite existing members", function() { + 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()).toEqual(oobMemberEvent.getId()); - expect(memberA.isOutOfBand()).toEqual(true); - }); - - it("should allow later state events to overwrite", function() { - const oobMemberEvent = utils.mkMembership({ - user: userA, mship: "join", room: roomId, event: true, - }); - const memberEvent = utils.mkMembership({ - user: userA, mship: "join", room: roomId, event: true, - }); - - state.markOutOfBandMembersStarted(); - state.setOutOfBandMembers([oobMemberEvent]); - state.setStateEvents([memberEvent]); - - const memberA = state.getMember(userA); - expect(memberA.events.member.getId()).toEqual(memberEvent.getId()); + 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: userA, mship: "join", room: roomId, event: true, + user: doesntExistYetUserId, mship: "join", room: roomId, event: true, }); let eventReceived = false; state.once('RoomState.members', (_, __, member) => { - expect(member.userId).toEqual(userA); + expect(member.userId).toEqual(doesntExistYetUserId); eventReceived = true; }); @@ -341,28 +325,6 @@ describe("RoomState", function() { state.setOutOfBandMembers([oobMemberEvent]); expect(eventReceived).toEqual(true); }); - - - it("should not overwrite members updated since starting loading oob", - function() { - const oobMemberEvent = utils.mkMembership({ - user: userA, mship: "join", room: roomId, event: true, - }); - - const existingMemberEvent = utils.mkMembership({ - user: userA, mship: "join", room: roomId, event: true, - }); - - state.markOutOfBandMembersStarted(); - state.setStateEvents([existingMemberEvent]); - expect(state.getMember(userA).supersedesOutOfBand()).toEqual(true); - state.setOutOfBandMembers([oobMemberEvent]); - - const memberA = state.getMember(userA); - expect(memberA.events.member.getId()).toEqual(existingMemberEvent.getId()); - expect(memberA.isOutOfBand()).toEqual(false); - expect(memberA.supersedesOutOfBand()).toEqual(false); - }); }); describe("clone", function() { @@ -386,20 +348,6 @@ describe("RoomState", function() { expect(state.getJoinedMemberCount()).toEqual(copy.getJoinedMemberCount()); }); - it("should copy supersedes flag when OOB loading is progress", - function() { - // include OOB members in copy - state.markOutOfBandMembersStarted(); - state.setStateEvents([utils.mkMembership({ - user: userA, mship: "join", room: roomId, event: true, - })]); - const copy = state.clone(); - const memberA = state.getMember(userA); - const memberACopy = copy.getMember(userA); - expect(memberA.supersedesOutOfBand()).toEqual(true); - expect(memberACopy.supersedesOutOfBand()).toEqual(true); - }); - it("should mark old copy as not waiting for out of band anymore", function() { state.markOutOfBandMembersStarted(); const copy = state.clone(); diff --git a/src/models/room-member.js b/src/models/room-member.js index 598cae398..e7a4bf88c 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -59,7 +59,6 @@ function RoomMember(roomId, userId) { member: null, }; this._isOutOfBand = false; - this._supersedesOutOfBand = false; this._updateModifiedTime(); } utils.inherits(RoomMember, EventEmitter); @@ -80,31 +79,6 @@ RoomMember.prototype.isOutOfBand = function() { return this._isOutOfBand; }; -/** - * Does the member supersede an incoming out-of-band - * member? If so the out-of-band member should be ignored. - * @return {bool} - */ -RoomMember.prototype.supersedesOutOfBand = function() { - return this._supersedesOutOfBand; -}; - -/** - * Mark the member as superseding the future incoming - * out-of-band members. - */ -RoomMember.prototype.markSupersedesOutOfBand = function() { - this._supersedesOutOfBand = true; -}; - -/** - * Clear the member superseding the future incoming - * out-of-band members, as loading finished or failed. - */ -RoomMember.prototype.clearSupersedesOutOfBand = function() { - this._supersedesOutOfBand = false; -}; - /** * Update this room member's membership event. May fire "RoomMember.name" if * this event updates this member's name. diff --git a/src/models/room-state.js b/src/models/room-state.js index 9a85e71ce..ac5e20077 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -220,8 +220,7 @@ RoomState.prototype.clone = function() { // 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 and - // copy the superseding flag from the current state + // after copying, we set back the status const status = this._oobMemberFlags.status; this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED; @@ -249,14 +248,6 @@ RoomState.prototype.clone = function() { copyMember.markOutOfBand(); } }); - } else if (this._oobMemberFlags.status == OOB_STATUS_INPROGRESS) { - // copy markSupersedesOutOfBand flags - this.getMembers().forEach((member) => { - if (member.supersedesOutOfBand()) { - const copyMember = copy.getMember(member.userId); - copyMember.markSupersedesOutOfBand(); - } - }); } return copy; @@ -341,11 +332,6 @@ RoomState.prototype.setStateEvents = function(stateEvents) { const member = self._getOrCreateMember(userId, event); member.setMembershipEvent(event, self); - // if out of band members are loading, - // mark the member as more recent - if (self._oobMemberFlags.status == OOB_STATUS_INPROGRESS) { - member.markSupersedesOutOfBand(); - } self._updateMember(member); self.emit("RoomState.members", event, self, member); @@ -434,12 +420,6 @@ RoomState.prototype.markOutOfBandMembersFailed = function() { if (this._oobMemberFlags.status !== OOB_STATUS_INPROGRESS) { return; } - // the request failed, there is nothing to supersede - // in case of a retry, these event would not supersede the - // retry anymore. - this.getMembers().forEach((m) => { - m.clearSupersedesOutOfBand(); - }); this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED; }; @@ -491,7 +471,7 @@ RoomState.prototype._setOutOfBandMember = function(stateEvent) { const member = this._getOrCreateMember(userId, stateEvent); member.setMembershipEvent(stateEvent, this); // needed to know which members need to be stored seperately - // as the are not part of the sync accumulator + // as they are not part of the sync accumulator // this is cleared by setMembershipEvent so when it's updated through /sync member.markOutOfBand(); From c0ae78ae82ef9765ce892844dcb34654c6bdb7f2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 11 Oct 2018 14:42:29 +0100 Subject: [PATCH 322/472] Prepare changelog for v0.12.0-rc.1 --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 896e3667b..255527f7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +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) + + * 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) + Latest Changes: =============== From 6aff3ed407ddd94daef5ca001c209eef02819d2a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 11 Oct 2018 14:42:29 +0100 Subject: [PATCH 323/472] v0.12.0-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index db0b14bb1..3c4065e91 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.11.1", + "version": "0.12.0-rc.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From 874029dff0686c1b30ae41cb5811c7dd8cf60d3d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 11 Oct 2018 15:38:34 +0100 Subject: [PATCH 324/472] oops - fix changelog format in retrospect --- CHANGELOG.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 255527f7f..fe454f3ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ Changes in [0.12.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta ============================================================================================================ [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 @@ -25,17 +36,6 @@ Changes in [0.12.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/ta * Revert "Add getMediaLimits to client" [\#745](https://github.com/matrix-org/matrix-js-sdk/pull/745) -Latest Changes: -=============== - -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. - 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) From 9b12c228235e5f13dfe9664f00f827e094b49adf Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 12 Oct 2018 10:38:10 -0400 Subject: [PATCH 325/472] de-lint plus some minor fixes --- .eslintrc.js | 1 + spec/unit/crypto/backup.spec.js | 148 +++++++++++------- src/crypto/algorithms/megolm.js | 4 - src/crypto/index.js | 45 ++++-- .../store/indexeddb-crypto-store-backend.js | 19 +-- src/crypto/store/indexeddb-crypto-store.js | 9 +- 6 files changed, 138 insertions(+), 88 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index fec2d7b5a..ae1826de5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,6 +16,7 @@ module.exports = { }, extends: ["eslint:recommended", "google"], rules: { + "indent": ["error", 4], // rules we've always adhered to or now do "max-len": ["error", { code: 90, diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index b1948c8a2..7217f3226 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -37,6 +37,8 @@ if (global.Olm) { Crypto = require('../../../lib/crypto'); } +const Olm = global.Olm; + const MatrixClient = sdk.MatrixClient; const MatrixEvent = sdk.MatrixEvent; const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; @@ -93,16 +95,21 @@ describe("MegolmBackup", function() { let sessionStore; let cryptoStore; let megolmDecryption; - beforeEach(function() { + beforeEach(async function() { + await Olm.init(); testUtils.beforeEach(this); // eslint-disable-line no-invalid-this mockCrypto = testUtils.mock(Crypto, 'Crypto'); - mockCrypto.backupKey = new global.Olm.PkEncryption(); + mockCrypto.backupKey = new Olm.PkEncryption(); mockCrypto.backupKey.set_recipient_key( "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", ); mockCrypto.backupInfo = { + algorithm: "m.megolm_backup.v1", version: 1, + auth_data: { + public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", + }, }; mockStorage = new MockStorageApi(); @@ -136,7 +143,7 @@ describe("MegolmBackup", function() { }); it('automatically calls the key back up', function() { - const groupSession = new global.Olm.OutboundGroupSession(); + const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); // construct a fake decrypted key event via the use of a mocked @@ -161,8 +168,9 @@ describe("MegolmBackup", function() { mockCrypto.decryptEvent = function() { return Promise.resolve(decryptedData); }; + mockCrypto.cancelRoomKeyRequest = function() {}; - mockBaseApis.sendKeyBackup = expect.createSpy(); + mockCrypto.backupGroupSession = expect.createSpy(); return event.attemptDecryption(mockCrypto).then(() => { return megolmDecryption.onRoomKeyEvent(event); @@ -171,23 +179,23 @@ describe("MegolmBackup", function() { }); }); - it('sends backups to the server', function () { - this.timeout(12000); - const groupSession = new global.Olm.OutboundGroupSession(); + it('sends backups to the server', function() { + this.timeout(12000); // eslint-disable-line no-invalid-this + const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); - const ibGroupSession = new global.Olm.InboundGroupSession(); + 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; }, {}); + ].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; }, {}); + ].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)); @@ -216,32 +224,37 @@ describe("MegolmBackup", function() { 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", + 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), }, - room_id: ROOM_ID, - session: ibGroupSession.pickle(olmDevice._pickleKey), - }, - txn); - }); + txn); + }); }) .then(() => { client.enableKeyBackup({ - algorithm: "foobar", + algorithm: "m.megolm_backup.v1", version: 1, auth_data: { - public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK" + public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", }, }); let numCalls = 0; return new Promise((resolve, reject) => { - client._http.authedRequest = function(callback, method, path, queryParams, data, opts) { + client._http.authedRequest = function( + callback, method, path, queryParams, data, opts, + ) { expect(++numCalls <= 1); if (numCalls >= 2) { // exit out of retry loop if there's something wrong @@ -252,11 +265,19 @@ describe("MegolmBackup", function() { 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()); + 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()); + client._crypto.backupGroupSession( + "roomId", + "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", + [], + groupSession.session_id(), + groupSession.session_key(), + ); }) .then(() => { expect(numCalls).toBe(1); @@ -264,23 +285,23 @@ describe("MegolmBackup", function() { }); }); - it('retries when a backup fails', function () { - this.timeout(12000); - const groupSession = new global.Olm.OutboundGroupSession(); + it('retries when a backup fails', function() { + this.timeout(12000); // eslint-disable-line no-invalid-this + const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); - const ibGroupSession = new global.Olm.InboundGroupSession(); + 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; }, {}); + ].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; }, {}); + ].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)); @@ -309,32 +330,37 @@ describe("MegolmBackup", function() { 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", + 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), }, - room_id: ROOM_ID, - session: ibGroupSession.pickle(olmDevice._pickleKey), - }, - txn); - }); + txn); + }); }) .then(() => { client.enableKeyBackup({ algorithm: "foobar", version: 1, auth_data: { - public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmoK" + public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", }, }); let numCalls = 0; return new Promise((resolve, reject) => { - client._http.authedRequest = function(callback, method, path, queryParams, data, opts) { + client._http.authedRequest = function( + callback, method, path, queryParams, data, opts, + ) { expect(++numCalls <= 2); if (numCalls >= 3) { // exit out of retry loop if there's something wrong @@ -345,15 +371,25 @@ describe("MegolmBackup", function() { 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()); + 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")); + return Promise.reject( + new Error("this is an expected failure"), + ); } }; - client._crypto.backupGroupSession("roomId", "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", [], groupSession.session_id(), groupSession.session_key()); + client._crypto.backupGroupSession( + "roomId", + "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", + [], + groupSession.session_id(), + groupSession.session_key(), + ); }) .then(() => { expect(numCalls).toBe(2); @@ -369,13 +405,13 @@ describe("MegolmBackup", function() { const scheduler = [ "getQueueForEvent", "queueEvent", "removeEventFromQueue", "setProcessFunction", - ].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {}); + ].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; }, {}); + ].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)); @@ -411,7 +447,7 @@ describe("MegolmBackup", function() { }; return client.restoreKeyBackups( "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" - + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", + + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", ROOM_ID, SESSION_ID, ).then(() => { @@ -435,7 +471,7 @@ describe("MegolmBackup", function() { }; return client.restoreKeyBackups( "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" - + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", + + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", ).then(() => { return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); }).then((res) => { diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index 1e1de101b..af311e16b 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -849,10 +849,6 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { this._retryDecryption(senderKey, sessionId); }).then(() => { if (this._crypto.backupInfo) { - // XXX: No retries on this at all: if this request dies for whatever - // reason, this key will never be uploaded. - // More XXX: If this fails it'll cause the message send to fail, - // and this will happen if the backup is deleted from another client. return this._crypto.backupGroupSession( content.room_id, senderKey, forwardingKeyChain, content.session_id, content.session_key, keysClaimed, diff --git a/src/crypto/index.js b/src/crypto/index.js index 0ca0d8506..d45caabe3 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -991,33 +991,41 @@ Crypto.prototype._maybeSendKeyBackup = async function() { } const data = {}; for (const session of sessions) { - const room_id = session.sessionData.room_id; - if (data[room_id] === undefined) - data[room_id] = {sessions: {}}; + const roomId = session.sessionData.room_id; + if (data[roomId] === undefined) { + data[roomId] = {sessions: {}}; + } - const sessionData = await this._olmDevice.exportInboundGroupSession(session.senderKey, session.sessionId, session.sessionData); + const sessionData = await this._olmDevice.exportInboundGroupSession( + session.senderKey, session.sessionId, session.sessionData, + ); sessionData.algorithm = olmlib.MEGOLM_ALGORITHM; delete sessionData.session_id; delete sessionData.room_id; const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); - data[room_id]['sessions'][session.sessionId] = { + data[roomId]['sessions'][session.sessionId] = { first_message_index: 1, // FIXME - forwarded_count: (sessionData.forwardingCurve25519KeyChain || []).length, + forwarded_count: + (sessionData.forwardingCurve25519KeyChain || []).length, is_verified: false, // FIXME: how do we determine this? session_data: encrypted, }; } try { - await this._baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, {rooms: data}); + await this._baseApis.sendKeyBackup( + undefined, undefined, this.backupInfo.version, + {rooms: data}, + ); numFailures = 0; await this._cryptoStore.unmarkSessionsNeedingBackup(sessions); - } - catch (err) { + } catch (err) { numFailures++; console.log("send failed", err); - if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) { + if (err.httpStatus === 400 + || err.httpStatus === 403 + || err.httpStatus === 401) { // retrying probably won't help much, so we should give up // FIXME: disable backups? return; @@ -1026,17 +1034,18 @@ Crypto.prototype._maybeSendKeyBackup = async function() { if (numFailures) { // exponential backoff if we have failures await new Promise((resolve, reject) => { - setTimeout(resolve, 1000 * Math.pow(2, Math.min(numFailures - 1, 4))); + setTimeout( + resolve, + 1000 * Math.pow(2, Math.min(numFailures - 1, 4)), + ); }); } } - } - finally - { + } finally { this._sendingBackups = false; } } -} +}; Crypto.prototype.backupGroupSession = async function( roomId, senderKey, forwardingCurve25519KeyChain, @@ -1057,13 +1066,15 @@ Crypto.prototype.backupGroupSession = async function( Crypto.prototype.backupAllGroupSessions = async function(version) { await this._cryptoStore.doTxn( - 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS, IndexedDBCryptoStore.STORE_BACKUP], (txn) => { + 'readwrite', + [IndexedDBCryptoStore.STORE_SESSIONS, IndexedDBCryptoStore.STORE_BACKUP], + (txn) => { this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => { if (session !== null) { this._cryptoStore.markSessionsNeedingBackup([session], txn); } }); - } + }, ); this._maybeSendKeyBackup(); diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index 9935dbd38..d0bb9f1b7 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -466,31 +466,33 @@ export class Backend { return new Promise((resolve, reject) => { const sessions = []; - const txn = this._db.transaction(["sessions_needing_backup", "inbound_group_sessions"], "readonly"); + 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) + const sessionGetReq = sessionStore.get(cursor.key); sessionGetReq.onsuccess = function() { sessions.push({ senderKey: sessionGetReq.result.senderCurve25519Key, sessionId: sessionGetReq.result.sessionId, - sessionData: sessionGetReq.result.session + sessionData: sessionGetReq.result.session, }); - } - //sessions.push(cursor.value); + }; if (!limit || sessions.length < limit) { cursor.continue(); } } - } + }; }); } @@ -516,13 +518,12 @@ export class Backend { return new Promise((resolve, reject) => { const req = objectStore.put({ senderCurve25519Key: session.senderKey, - sessionId: session.sessionId + sessionId: session.sessionId, }); req.onsuccess = resolve; req.onerror = reject; }); })); - } doTxn(mode, stores, func) { diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 0eca4c373..59d68f8fc 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -407,10 +407,13 @@ 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) => { @@ -420,7 +423,8 @@ export default class IndexedDBCryptoStore { /** * Unmark sessions as needing to be backed up. - * @param {[object]} The sessions that need to be backed up. + * @param {[object]} sessions The sessions that need to be backed up. + * @returns {Promise} resolves when the sessions are unmarked */ unmarkSessionsNeedingBackup(sessions) { return this._connect().then((backend) => { @@ -430,8 +434,9 @@ export default class IndexedDBCryptoStore { /** * Mark sessions as needing to be backed up. - * @param {[object]} The sessions that need to be backed up. + * @param {[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) => { From 91fb7b0a7c4d530c33f2e47a769fc741302fa0a8 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 12 Oct 2018 12:03:51 -0400 Subject: [PATCH 326/472] fix unit tests for backup recovery --- spec/unit/crypto/backup.spec.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 7217f3226..3a3ed3a97 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -446,8 +446,7 @@ describe("MegolmBackup", function() { return Promise.resolve(KEY_BACKUP_DATA); }; return client.restoreKeyBackups( - "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" - + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", + "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", ROOM_ID, SESSION_ID, ).then(() => { @@ -463,15 +462,14 @@ describe("MegolmBackup", function() { rooms: { [ROOM_ID]: { sessions: { - SESSION_ID: KEY_BACKUP_DATA, + [SESSION_ID]: KEY_BACKUP_DATA, }, }, }, }); }; return client.restoreKeyBackups( - "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" - + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", + "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", ).then(() => { return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); }).then((res) => { From d49c0a1bcb5a53b5cf90bfbef968ce6e2ac12ccc Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 12 Oct 2018 14:28:31 -0400 Subject: [PATCH 327/472] more de-linting and fixing --- src/crypto/algorithms/megolm.js | 19 ++++++-- src/crypto/index.js | 4 +- .../store/indexeddb-crypto-store-backend.js | 1 - src/crypto/store/localStorage-crypto-store.js | 45 +++++++++++-------- src/crypto/store/memory-crypto-store.js | 10 +++-- 5 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index af311e16b..d1115f00e 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -264,8 +264,8 @@ MegolmEncryption.prototype._prepareNewSession = async function() { ); if (this._crypto.backupInfo) { - // Not strictly necessary to wait for this - await this._crypto.backupGroupSession( + // don't wait for it to complete + this._crypto.backupGroupSession( this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId, key.key, ); @@ -849,7 +849,8 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { this._retryDecryption(senderKey, sessionId); }).then(() => { if (this._crypto.backupInfo) { - return this._crypto.backupGroupSession( + // don't wait for it to complete + this._crypto.backupGroupSession( content.room_id, senderKey, forwardingKeyChain, content.session_id, content.session_key, keysClaimed, exportFormat, @@ -972,6 +973,18 @@ 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, + ); + } // have another go at decrypting events sent with this session. this._retryDecryption(session.sender_key, session.session_id); }); diff --git a/src/crypto/index.js b/src/crypto/index.js index d45caabe3..20341c4b2 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1061,7 +1061,7 @@ Crypto.prototype.backupGroupSession = async function( sessionId: sessionId, }]); - this._maybeSendKeyBackup(); + await this._maybeSendKeyBackup(); }; Crypto.prototype.backupAllGroupSessions = async function(version) { @@ -1077,7 +1077,7 @@ Crypto.prototype.backupAllGroupSessions = async function(version) { }, ); - this._maybeSendKeyBackup(); + await this._maybeSendKeyBackup(); }; /* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307 diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index d0bb9f1b7..d5b66c30f 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -501,7 +501,6 @@ export class Backend { const objectStore = txn.objectStore("sessions_needing_backup"); return Promise.all(sessions.map((session) => { return new Promise((resolve, reject) => { - console.log(session); const req = objectStore.delete([session.senderKey, session.sessionId]); req.onsuccess = resolve; req.onerror = reject; diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index 71a904fd8..cad6a7d64 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -167,43 +167,52 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { } getSessionsNeedingBackup(limit) { + const sessionsNeedingBackup + = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; const sessions = []; - for (const session in getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP)) { - const senderKey = session.substr(0, 43); - const sessionId = session.substr(44); - getEndToEndInboundGroupSession(senderKey, sessionId, null, (sessionData) => { - sessions.push({ - senderKey: senderKey, - sessionId: sessionId, - sessionData: sessionData, - }); - }) - if (limit && session.length >= limit) { - break; + for (const session in sessionsNeedingBackup) { + if (Object.prototype.hasOwnProperty.call(sessionsNeedingBackup, session)) { + 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); } unmarkSessionsNeedingBackup(sessions) { - const sessionsNeedingBackup = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; - for(const session of 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_SESSION_NEEDING_BACKUP, sessionsNeedinBackup, + 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) { + const sessionsNeedingBackup + = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + for (const session of sessions) { sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true; } setJsonItem( - this.store, KEY_SESSION_NEEDING_BACKUP, sessionsNeedinBackup, + this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup, ); return Promise.resolve(); } diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index cd5ec5be4..6af312219 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -315,15 +315,17 @@ export default class MemoryCryptoStore { } unmarkSessionsNeedingBackup(sessions) { - for(const session of sessions) { - delete this._sessionsNeedingBackup[session.senderKey + '/' + session.sessionId]; + 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) { - this._sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true; + for (const session of sessions) { + const sessionKey = session.senderKey + '/' + session.sessionId; + this._sessionsNeedingBackup[sessionKey] = true; } return Promise.resolve(); } From 40d0a823428e331063670b4978f07cc5fc7ca774 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 12 Oct 2018 15:45:48 -0400 Subject: [PATCH 328/472] remove accidental change to eslintrc --- .eslintrc.js | 1 - 1 file changed, 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index ae1826de5..fec2d7b5a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,7 +16,6 @@ module.exports = { }, extends: ["eslint:recommended", "google"], rules: { - "indent": ["error", 4], // rules we've always adhered to or now do "max-len": ["error", { code: 90, From b9e198c172906d287b8c0257e6db1b6ec617c754 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 15 Oct 2018 11:39:39 +0100 Subject: [PATCH 329/472] Oops: remove debug logging --- spec/unit/crypto.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js index 1b28ad683..d48879882 100644 --- a/spec/unit/crypto.spec.js +++ b/spec/unit/crypto.spec.js @@ -18,7 +18,6 @@ describe("Crypto", function() { }); it("Crypto exposes the correct olm library version", function() { - console.log(Crypto); expect(Crypto.getOlmVersion()[0]).toEqual(2); }); }); From 68497d3a1fea6d85687338862e72587294ee4ae7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 16 Oct 2018 10:47:32 +0100 Subject: [PATCH 330/472] Prepare changelog for v0.12.0 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe454f3ac..82902fc22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +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) From 874bdea634677dda51d163b2815cb1cc28935013 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 16 Oct 2018 10:47:33 +0100 Subject: [PATCH 331/472] v0.12.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3c4065e91..cc5a7a0e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.12.0-rc.1", + "version": "0.12.0", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From 434ac86090836b1c1b2e8820d30e505aacd6f19b Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 19 Oct 2018 10:51:19 -0400 Subject: [PATCH 332/472] properly fill out the is_verified and first_message_index fields --- src/crypto/DeviceList.js | 41 +++++++++++++++++++++++++++++++++++++++- src/crypto/OlmDevice.js | 1 + src/crypto/index.js | 17 +++++++++++++---- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index fa55f2fa6..c3a86ae1e 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -71,6 +71,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() @@ -128,6 +131,19 @@ export default class DeviceList { deviceData.trackingStatus : {}; this._syncToken = deviceData ? deviceData.syncToken : null; } + this._userByIdentityKey = {}; + for (const user in this._devices) { + if (!this._devices.hasOwnProperty(user)) { + continue; + } + const userDevices = this._devices[user]; + for (const device in userDevices) { + if (!userDevices.hasOwnProperty(device)) { + continue; + } + this._userByIdentityKey[userDevices[device].senderKey] = user; + } + } }); }, ); @@ -357,13 +373,24 @@ export default class DeviceList { /** * Find a device by curve25519 identity key * - * @param {string} userId owner of the device + * @param {string} userId owner of the device (optional) * @param {string} algorithm encryption algorithm * @param {string} senderKey curve25519 key to match * * @return {module:crypto/deviceinfo?} */ getDeviceByIdentityKey(userId, algorithm, senderKey) { + if (arguments.length === 2) { + // if userId is omitted, shift the other arguments, and look up the + // userid + senderKey = algorithm; + algorithm = userId; + userId = this._userByIdentityKey[senderKey]; + if (!userId) { + return null; + } + } + if ( algorithm !== olmlib.OLM_ALGORITHM && algorithm !== olmlib.MEGOLM_ALGORITHM @@ -409,6 +436,12 @@ export default class DeviceList { */ storeDevicesForUser(u, devs) { this._devices[u] = devs; + for (const device in devs) { + if (!devs.hasOwnProperty(device)) { + continue; + } + this._userByIdentityKey[devs[device].senderKey] = u; + } this._dirty = true; } @@ -526,6 +559,12 @@ export default class DeviceList { */ _setRawStoredDevicesForUser(userId, devices) { this._devices[userId] = devices; + for (const device in devices) { + if (!devices.hasOwnProperty(device)) { + continue; + } + this._userByIdentityKey[devices[device].senderKey] = userId; + } } /** diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 818840054..74e46e2a4 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -1119,6 +1119,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(), }; }); }; diff --git a/src/crypto/index.js b/src/crypto/index.js index 20341c4b2..2f1c39bd9 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -940,6 +940,7 @@ Crypto.prototype.exportRoomKeys = async function() { const sess = this._olmDevice.exportInboundGroupSession( s.senderKey, s.sessionId, s.sessionData, ); + delete sess.first_known_index; sess.algorithm = olmlib.MEGOLM_ALGORITHM; exportedSessions.push(sess); }); @@ -1002,13 +1003,21 @@ Crypto.prototype._maybeSendKeyBackup = async function() { sessionData.algorithm = olmlib.MEGOLM_ALGORITHM; delete sessionData.session_id; delete sessionData.room_id; + const firstKnownIndex = sessionData.first_known_index; + delete sessionData.first_known_index; const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); + const forwardedCount = + (sessionData.forwardingCurve25519KeyChain || []).length; + + const device = this._deviceList.getDeviceByIdentityKey( + olmlib.MEGOLM_ALGORITHM, session.senderKey, + ); + data[roomId]['sessions'][session.sessionId] = { - first_message_index: 1, // FIXME - forwarded_count: - (sessionData.forwardingCurve25519KeyChain || []).length, - is_verified: false, // FIXME: how do we determine this? + first_message_index: firstKnownIndex, + forwarded_count: forwardedCount, + is_verified: !!(device && device.isVerified()), session_data: encrypted, }; } From d8bcc4e3f1bd743810c5b07d00baaa9628ff5cda Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 19 Oct 2018 13:34:22 -0600 Subject: [PATCH 333/472] 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. --- src/client.js | 11 ++++++++++- src/http-api.js | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index be4e5857b..1956b9225 100644 --- a/src/client.js +++ b/src/client.js @@ -921,6 +921,8 @@ MatrixClient.prototype.isUserIgnored = function(userId) { * Default: true. * @param {boolean} opts.inviteSignUrl If the caller has a keypair 3pid invite, * the signing URL is passed in this parameter. + * @param {string[]} opts.viaServers The server names to try and join through in + * addition to those that are automatically chosen. * @param {module:client.callback} callback Optional. * @return {module:client.Promise} Resolves: Room object. * @return {module:http-api.MatrixError} Rejects: with an error response. @@ -949,6 +951,13 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) { ); } + const queryString = {}; + if (opts.viaServers) { + queryString["server_name"] = opts.viaServers; + } + + const reqOpts = {qsStringifyOptions: {arrayFormat: 'repeat'}}; + const defer = Promise.defer(); const self = this; @@ -959,7 +968,7 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) { } const path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias}); - return self._http.authedRequest(undefined, "POST", path, undefined, data); + return self._http.authedRequest(undefined, "POST", path, queryString, data, reqOpts); }).then(function(res) { const roomId = res.room_id; const syncApi = new SyncApi(self, self._clientOpts); diff --git a/src/http-api.js b/src/http-api.js index b753d6bf8..ca71d5668 100644 --- a/src/http-api.js +++ b/src/http-api.js @@ -668,6 +668,9 @@ module.exports.MatrixHttpApi.prototype = { * @param {function=} opts.bodyParser function to parse the body of the * response before passing it to the promise and callback. * + * @param (object=} opts.qsStringifyOptions options for stringifying the + * query string. + * * @return {module:client.Promise} a promise which resolves to either the * response object (if this.opts.onlyData is truthy), or the parsed * body. Rejects @@ -752,6 +755,8 @@ module.exports.MatrixHttpApi.prototype = { method: method, withCredentials: false, qs: queryParams, + //qsStringifyOptions: opts.qsStringifyOptions, + useQuerystring: true, body: data, json: false, timeout: localTimeoutMs, From 8d35bea830205073ca5c5ac71442c7b5b69b040d Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Sat, 20 Oct 2018 21:37:55 -0500 Subject: [PATCH 334/472] Add repository type to package.json to make it valid Signed-off-by: Aaron Raimist --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index cc5a7a0e5..88f87f90b 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt" }, "repository": { + "type": "git", "url": "https://github.com/matrix-org/matrix-js-sdk" }, "keywords": [ From 322ef1fd637003889fe3825957c66d3e8c39abf2 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Mon, 22 Oct 2018 11:28:16 -0400 Subject: [PATCH 335/472] update backup algorithm name to agree with the proposal --- src/crypto/olmlib.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/olmlib.js b/src/crypto/olmlib.js index f03714f16..bbe942036 100644 --- a/src/crypto/olmlib.js +++ b/src/crypto/olmlib.js @@ -38,7 +38,7 @@ module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2"; /** * matrix algorithm tag for megolm backups */ -module.exports.MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1"; +module.exports.MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2"; /** From d0e1471c91c5e417e93c4ebd67680341135b6897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Jaenisch?= Date: Tue, 23 Oct 2018 23:57:52 +0200 Subject: [PATCH 336/472] Added loglevel library. Refs #332 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Jaenisch --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 88f87f90b..d8a97f9fd 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "expect": "^1.20.2", "istanbul": "^0.4.5", "jsdoc": "^3.5.5", + "loglevel": "1.6.1", "lolex": "^1.5.2", "matrix-mock-request": "^1.2.0", "mocha": "^5.2.0", From bbb8e12bacf4a4ef445667cb71505cd5224a404c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Jaenisch?= Date: Wed, 24 Oct 2018 00:29:26 +0200 Subject: [PATCH 337/472] Create logger module. Refs #332 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Jaenisch --- src/logger.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/logger.js diff --git a/src/logger.js b/src/logger.js new file mode 100644 index 000000000..69491b249 --- /dev/null +++ b/src/logger.js @@ -0,0 +1,21 @@ +"use strict"; +/** + * @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); +log.setDefaultLevel(log.levels.WARN); + +/** + * Drop-in replacement for console using {@link https://www.npmjs.com/package/loglevel|loglevel}. + * Can be tailored down to specific use cases if needed. +*/ +module.exports.logger = logger; From f41060c39a5385348e4dd71cdd54d2f87d5eb76c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Jaenisch?= Date: Wed, 24 Oct 2018 00:47:51 +0200 Subject: [PATCH 338/472] Replace console.log with loglevel logger. Fixes #332 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Jaenisch --- src/crypto/DeviceList.js | 45 ++++++++-------- src/crypto/OlmDevice.js | 15 +++--- src/crypto/OutgoingRoomKeyRequestManager.js | 21 ++++---- src/crypto/algorithms/megolm.js | 33 ++++++------ src/crypto/algorithms/olm.js | 5 +- src/crypto/index.js | 51 ++++++++++--------- src/crypto/olmlib.js | 11 ++-- .../store/indexeddb-crypto-store-backend.js | 16 +++--- src/crypto/store/indexeddb-crypto-store.js | 19 +++---- src/crypto/store/localStorage-crypto-store.js | 6 ++- src/crypto/store/memory-crypto-store.js | 9 ++-- 11 files changed, 122 insertions(+), 109 deletions(-) diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index fa55f2fa6..f86c9ff99 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -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'; @@ -110,7 +111,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() || {} @@ -190,7 +191,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 +259,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 +270,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(() => { @@ -433,7 +434,7 @@ 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 @@ -452,7 +453,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 +488,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 @@ -550,7 +551,7 @@ export default class DeviceList { ).then(() => { finished(true); }, (e) => { - console.error( + logger.error( 'Error downloading keys for ' + users + ":", e, ); finished(false); @@ -573,7 +574,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 +585,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 +660,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 +680,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 +707,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 +717,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 +726,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 +764,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 +781,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 +816,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 +826,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 +843,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; } diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index cda14779c..01cd3a340 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -15,6 +15,7 @@ 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'; /** @@ -173,7 +174,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 +196,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 +208,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() { ); } } - console.log( + logger.log( "Migrating " + numSessions + " sessions from session store", ); }); @@ -236,14 +237,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", ); @@ -889,7 +890,7 @@ OlmDevice.prototype.addInboundGroupSession = async function( roomId, senderKey, sessionId, txn, (existingSession, existingSessionData) => { if (existingSession) { - console.log( + logger.log( "Update for megolm session " + senderKey + "/" + sessionId, ); // for now we just ignore updates. TODO: implement something here @@ -1034,7 +1035,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}, ` + diff --git a/src/crypto/OutgoingRoomKeyRequestManager.js b/src/crypto/OutgoingRoomKeyRequestManager.js index 75ab35454..4c9b7cbf5 100644 --- a/src/crypto/OutgoingRoomKeyRequestManager.js +++ b/src/crypto/OutgoingRoomKeyRequestManager.js @@ -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; } @@ -173,7 +174,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), ); @@ -201,7 +202,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', @@ -222,7 +223,7 @@ export default class OutgoingRoomKeyRequestManager { updatedReq, andResend, ).catch((e) => { - console.error( + logger.error( "Error sending room key request cancellation;" + " will retry later.", e, ); @@ -261,7 +262,7 @@ 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(); @@ -282,7 +283,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 +291,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,7 +313,7 @@ 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(); @@ -321,7 +322,7 @@ export default class OutgoingRoomKeyRequestManager { // 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})`, @@ -347,7 +348,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)} ` + diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index bda57fe33..d8d8fd8f2 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -24,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"); @@ -65,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", ); @@ -103,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; } @@ -113,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, ); @@ -182,7 +183,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; } @@ -192,7 +193,7 @@ 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(); } @@ -319,7 +320,7 @@ MegolmEncryption.prototype._splitUserDeviceMap = function( continue; } - console.log( + logger.log( "share keys with device " + userId + ":" + deviceId, ); @@ -441,10 +442,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; @@ -463,7 +464,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. @@ -782,12 +783,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; } @@ -804,13 +805,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; @@ -823,7 +824,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { keysClaimed = event.getKeysClaimed(); } - console.log(`Adding key for megolm session ${senderKey}|${sessionId}`); + logger.log(`Adding key for megolm session ${senderKey}|${sessionId}`); this._olmDevice.addInboundGroupSession( content.room_id, senderKey, forwardingKeyChain, sessionId, content.session_key, keysClaimed, @@ -840,7 +841,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { // have another go at decrypting events sent with this session. this._retryDecryption(senderKey, sessionId); }).catch((e) => { - console.error(`Error handling m.room_key_event: ${e}`); + logger.error(`Error handling m.room_key_event: ${e}`); }); }; @@ -882,7 +883,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, diff --git a/src/crypto/algorithms/olm.js b/src/crypto/algorithms/olm.js index 69390b064..2ef4bdebb 100644 --- a/src/crypto/algorithms/olm.js +++ b/src/crypto/algorithms/olm.js @@ -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"); @@ -273,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, ); @@ -328,7 +329,7 @@ OlmDecryption.prototype._decryptMessage = async function( ); } - console.log( + logger.log( "created new inbound Olm session ID " + res.session_id + " with " + theirDeviceIdentityKey, ); diff --git a/src/crypto/index.js b/src/crypto/index.js index f00477d4b..f0d75380a 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -25,6 +25,7 @@ const anotherjson = require('another-json'); import Promise from 'bluebird'; import {EventEmitter} from 'events'; +const logger = require("../logger"); const utils = require("../utils"); const OlmDevice = require("./OlmDevice"); const olmlib = require("./olmlib"); @@ -196,7 +197,7 @@ Crypto.prototype.registerEventHandlers = function(eventEmitter) { try { crypto._onRoomMembership(event, member, oldMembership); } catch (e) { - console.error("Error handling membership change:", e); + logger.error("Error handling membership change:", e); } }); @@ -381,7 +382,7 @@ function _maybeUploadOneTimeKeys(crypto) { // create any more keys. return uploadLoop(keyCount); }).catch((e) => { - console.error("Error uploading one-time keys", e.stack || e); + logger.error("Error uploading one-time keys", e.stack || e); }).finally(() => { // reset _oneTimeKeyCount to prevent start uploading based on old data. // it will be set again on the next /sync-response @@ -606,13 +607,13 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) { const claimedKey = event.getClaimedEd25519Key(); if (!claimedKey) { - console.warn("Event " + event.getId() + " claims no ed25519 key: " + + logger.warn("Event " + event.getId() + " claims no ed25519 key: " + "cannot verify sending device"); return null; } if (claimedKey !== device.getFingerprint()) { - console.warn( + logger.warn( "Event " + event.getId() + " claims ed25519 key " + claimedKey + "but sender device has key " + device.getFingerprint()); return null; @@ -658,7 +659,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic const existingConfig = this._roomList.getRoomEncryption(roomId); if (existingConfig) { if (JSON.stringify(existingConfig) != JSON.stringify(config)) { - console.error("Ignoring m.room.encryption event which requests " + + logger.error("Ignoring m.room.encryption event which requests " + "a change of config in " + roomId); return; } @@ -705,7 +706,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic } if (!this._lazyLoadMembers) { - console.log("Enabling encryption in " + roomId + "; " + + logger.log("Enabling encryption in " + roomId + "; " + "starting to track device lists for all users therein"); await this.trackRoomDevices(roomId); @@ -717,7 +718,7 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic this._deviceList.refreshOutdatedDeviceLists(); } } else { - console.log("Enabling encryption in " + roomId); + logger.log("Enabling encryption in " + roomId); } }; @@ -738,7 +739,7 @@ Crypto.prototype.trackRoomDevices = function(roomId) { if (!room) { throw new Error(`Unable to start tracking devices in unknown room ${roomId}`); } - console.log(`Starting to track devices for room ${roomId} ...`); + logger.log(`Starting to track devices for room ${roomId} ...`); const members = await room.getEncryptionTargetMembers(); members.forEach((m) => { this._deviceList.startTrackingDeviceList(m.userId); @@ -834,7 +835,7 @@ Crypto.prototype.importRoomKeys = function(keys) { return Promise.map( keys, (key) => { if (!key.room_id || !key.algorithm) { - console.warn("ignoring room key entry with missing fields", key); + logger.warn("ignoring room key entry with missing fields", key); return null; } @@ -963,7 +964,7 @@ Crypto.prototype.requestRoomKey = function(requestBody, recipients) { requestBody, recipients, ).catch((e) => { // this normally means we couldn't talk to the store - console.error( + logger.error( 'Error requesting key for event', e, ); }).done(); @@ -980,7 +981,7 @@ Crypto.prototype.requestRoomKey = function(requestBody, recipients) { Crypto.prototype.cancelRoomKeyRequest = function(requestBody, andResend) { this._outgoingRoomKeyRequestManager.cancelRoomKeyRequest(requestBody, andResend) .catch((e) => { - console.warn("Error clearing pending room key requests", e); + logger.warn("Error clearing pending room key requests", e); }).done(); }; @@ -998,7 +999,7 @@ Crypto.prototype.onCryptoEvent = async function(event) { // finished processing the sync, in onSyncCompleted. await this.setRoomEncryption(roomId, content, true); } catch (e) { - console.error("Error configuring encryption in room " + roomId + + logger.error("Error configuring encryption in room " + roomId + ":", e); } }; @@ -1014,7 +1015,7 @@ Crypto.prototype.onSyncWillProcess = async function(syncData) { // scratch, so mark everything as untracked. onCryptoEvent will // be called for all e2e rooms during the processing of the sync, // at which point we'll start tracking all the users of that room. - console.log("Initial sync performed - resetting device tracking state"); + logger.log("Initial sync performed - resetting device tracking state"); this._deviceList.stopTrackingAllDeviceLists(); this._roomDeviceTrackingState = {}; } @@ -1132,7 +1133,7 @@ Crypto.prototype._onToDeviceEvent = function(event) { }); } } catch (e) { - console.error("Error handling toDeviceEvent:", e); + logger.error("Error handling toDeviceEvent:", e); } }; @@ -1146,7 +1147,7 @@ Crypto.prototype._onRoomKeyEvent = function(event) { const content = event.getContent(); if (!content.room_id || !content.algorithm) { - console.error("key event is missing fields"); + logger.error("key event is missing fields"); return; } @@ -1184,12 +1185,12 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) { // by calling _trackRoomDevices if (this._roomDeviceTrackingState[roomId]) { if (member.membership == 'join') { - console.log('Join event for ' + member.userId + ' in ' + roomId); + logger.log('Join event for ' + member.userId + ' in ' + roomId); // make sure we are tracking the deviceList for this user this._deviceList.startTrackingDeviceList(member.userId); } else if (member.membership == 'invite' && this._clientStore.getRoom(roomId).shouldEncryptForInvitedMembers()) { - console.log('Invite event for ' + member.userId + ' in ' + roomId); + logger.log('Invite event for ' + member.userId + ' in ' + roomId); this._deviceList.startTrackingDeviceList(member.userId); } } @@ -1256,7 +1257,7 @@ Crypto.prototype._processReceivedRoomKeyRequests = async function() { this._processReceivedRoomKeyRequestCancellation(cancellation), ); } catch (e) { - console.error(`Error processing room key requsts: ${e}`); + logger.error(`Error processing room key requsts: ${e}`); } finally { this._processingRoomKeyRequests = false; } @@ -1275,13 +1276,13 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) { const roomId = body.room_id; const alg = body.algorithm; - console.log(`m.room_key_request from ${userId}:${deviceId}` + + logger.log(`m.room_key_request from ${userId}:${deviceId}` + ` for ${roomId} / ${body.session_id} (id ${req.requestId})`); if (userId !== this._userId) { // TODO: determine if we sent this device the keys already: in // which case we can do so again. - console.log("Ignoring room key request from other user for now"); + logger.log("Ignoring room key request from other user for now"); return; } @@ -1291,18 +1292,18 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) { // if we don't have a decryptor for this room/alg, we don't have // the keys for the requested events, and can drop the requests. if (!this._roomDecryptors[roomId]) { - console.log(`room key request for unencrypted room ${roomId}`); + logger.log(`room key request for unencrypted room ${roomId}`); return; } const decryptor = this._roomDecryptors[roomId][alg]; if (!decryptor) { - console.log(`room key request for unknown alg ${alg} in room ${roomId}`); + logger.log(`room key request for unknown alg ${alg} in room ${roomId}`); return; } if (!await decryptor.hasKeysForKeyRequest(req)) { - console.log( + logger.log( `room key request for unknown session ${roomId} / ` + body.session_id, ); @@ -1316,7 +1317,7 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) { // if the device is is verified already, share the keys const device = this._deviceList.getStoredDevice(userId, deviceId); if (device && device.isVerified()) { - console.log('device is already verified: sharing keys'); + logger.log('device is already verified: sharing keys'); req.share(); return; } @@ -1333,7 +1334,7 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) { Crypto.prototype._processReceivedRoomKeyRequestCancellation = async function( cancellation, ) { - console.log( + logger.log( `m.room_key_request cancellation for ${cancellation.userId}:` + `${cancellation.deviceId} (id ${cancellation.requestId})`, ); diff --git a/src/crypto/olmlib.js b/src/crypto/olmlib.js index 56799c513..49ec4c20e 100644 --- a/src/crypto/olmlib.js +++ b/src/crypto/olmlib.js @@ -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"); /** @@ -65,7 +66,7 @@ module.exports.encryptMessageForDevice = async function( return; } - console.log( + logger.log( "Using sessionid " + sessionId + " for device " + recipientUserId + ":" + recipientDevice.deviceId, ); @@ -190,7 +191,7 @@ module.exports.ensureOlmSessionsForDevices = async function( } if (!oneTimeKey) { - console.warn( + logger.warn( "No one-time keys (alg=" + oneTimeKeyAlgorithm + ") for device " + userId + ":" + deviceId, ); @@ -219,7 +220,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 +234,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; } diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index 4a7f48789..96bbec68e 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -16,6 +16,8 @@ limitations under the License. */ import Promise from 'bluebird'; + +import logger from '../../logger'; import utils from '../../utils'; export const VERSION = 6; @@ -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, ); @@ -226,7 +228,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 +266,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})`, ); @@ -400,7 +402,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, ); @@ -471,7 +473,7 @@ export class Backend { } export function upgradeDatabase(db, oldVersion) { - console.log( + logger.log( `Upgrading IndexedDBCryptoStore from version ${oldVersion}` + ` to ${VERSION}`, ); diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 0e0654deb..c9210da23 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -17,6 +17,7 @@ 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'; @@ -64,7 +65,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 +78,7 @@ export default class IndexedDBCryptoStore { }; req.onblocked = () => { - console.log( + logger.log( `can't yet open IndexedDBCryptoStore because it is open elsewhere`, ); }; @@ -89,7 +90,7 @@ 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) => { @@ -106,13 +107,13 @@ export default class IndexedDBCryptoStore { }, ); }).catch((e) => { - console.warn( + logger.warn( `unable to connect to indexeddb ${this._dbName}` + `: falling back to localStorage store: ${e}`, ); return new LocalStorageCryptoStore(global.localStorage); }).catch((e) => { - console.warn( + logger.warn( `unable to open localStorage: falling back to in-memory store: ${e}`, ); return new MemoryCryptoStore(); @@ -133,11 +134,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`, ); }; @@ -147,14 +148,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}`); }); } diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index 3f2f0d09a..ed0b7ede4 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -15,6 +15,8 @@ limitations under the License. */ import Promise from 'bluebird'; + +import logger from '../../logger'; import MemoryCryptoStore from './memory-crypto-store.js'; /** @@ -199,8 +201,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; } diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 469cdb49b..4c2baf9e1 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -17,6 +17,7 @@ limitations under the License. import Promise from 'bluebird'; +import logger from '../../logger'; import utils from '../../utils'; /** @@ -71,7 +72,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 +82,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, ); @@ -163,7 +164,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 +195,7 @@ export default class MemoryCryptoStore { } if (req.state != expectedState) { - console.warn( + logger.warn( `Cannot delete room key request in state ${req.state} ` + `(expected ${expectedState})`, ); From 0f39a45734226f0f6c244a10abaff10ba01c734c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Jaenisch?= Date: Wed, 24 Oct 2018 01:44:10 +0200 Subject: [PATCH 339/472] Fixing module export of logger. Refs #332 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Jaenisch --- src/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logger.js b/src/logger.js index 69491b249..4147a2a1a 100644 --- a/src/logger.js +++ b/src/logger.js @@ -18,4 +18,4 @@ log.setDefaultLevel(log.levels.WARN); * Drop-in replacement for console using {@link https://www.npmjs.com/package/loglevel|loglevel}. * Can be tailored down to specific use cases if needed. */ -module.exports.logger = logger; +module.exports = logger; From 73dd07aadf66cdffb7ab415dc0676d3836804872 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 24 Oct 2018 11:14:59 +0100 Subject: [PATCH 340/472] Prepare changelog for v0.12.1-rc.1 --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82902fc22..bb0a47560 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +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) From f5832423f420b2b688c0674c09c5c6b069ff5db7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 24 Oct 2018 11:14:59 +0100 Subject: [PATCH 341/472] v0.12.1-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 88f87f90b..211c3c30b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.12.0", + "version": "0.12.1-rc.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From 84b91d4575d4ca8bdd545e5235c335c9e227c903 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 24 Oct 2018 16:58:48 +0100 Subject: [PATCH 342/472] Update to Olm 3 --- README.md | 4 ++-- travis.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ec11da0a3..6ba36d944 100644 --- a/README.md +++ b/README.md @@ -267,13 +267,13 @@ 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`` + * ``npm install 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 +can use ``npm install https://matrix.org/packages/npm/olm/olm-3.0.0.tgz --save-optional`` (if your application also works without e2e crypto enabled) or ``--save`` (if it doesn't) to do so. diff --git a/travis.sh b/travis.sh index 68d915def..ddf4a790e 100755 --- a/travis.sh +++ b/travis.sh @@ -5,7 +5,7 @@ set -ex npm run 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 install https://matrix.org/packages/npm/olm/olm-3.0.0.tgz npm run test From 40cb37e824b9a6fd92c92c9770af3e34242ed583 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 24 Oct 2018 16:58:48 +0100 Subject: [PATCH 343/472] Update to Olm 3 --- README.md | 4 ++-- travis.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 20dd640b6..be2fa0f86 100644 --- a/README.md +++ b/README.md @@ -319,13 +319,13 @@ 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`` + * ``npm install 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 +can use ``npm install https://matrix.org/packages/npm/olm/olm-3.0.0.tgz --save-optional`` (if your application also works without e2e crypto enabled) or ``--save`` (if it doesn't) to do so. diff --git a/travis.sh b/travis.sh index 4c47f00e7..ddf4a790e 100755 --- a/travis.sh +++ b/travis.sh @@ -5,7 +5,7 @@ set -ex npm run lint # install Olm so that we can run the crypto tests. -npm install https://matrix.org/packages/npm/olm/olm-2.3.0.tgz +npm install https://matrix.org/packages/npm/olm/olm-3.0.0.tgz npm run test From 3a316de9ef5ca23c7970953b40e1bd8e4ffb148a Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 24 Oct 2018 17:40:58 +0100 Subject: [PATCH 344/472] Update to Olm 3 here too --- spec/unit/crypto.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js index d48879882..ee06ef369 100644 --- a/spec/unit/crypto.spec.js +++ b/spec/unit/crypto.spec.js @@ -18,6 +18,6 @@ describe("Crypto", function() { }); it("Crypto exposes the correct olm library version", function() { - expect(Crypto.getOlmVersion()[0]).toEqual(2); + expect(Crypto.getOlmVersion()[0]).toEqual(3); }); }); From d29ac088c0618007b7abb7146ccbf1409db5d6ac Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 24 Oct 2018 18:55:04 +0100 Subject: [PATCH 345/472] retest From a8e2727473cd03bcd848fea7b5b23158df727638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Jaenisch?= Date: Wed, 24 Oct 2018 20:55:22 +0200 Subject: [PATCH 346/472] Set level of logger instead of all of them. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Jaenisch --- src/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logger.js b/src/logger.js index 4147a2a1a..9597df8ce 100644 --- a/src/logger.js +++ b/src/logger.js @@ -12,7 +12,7 @@ const log = require("loglevel"); // Part of #332 is introducing a logging library in the first place. const DEFAULT_NAME_SPACE = "matrix"; const logger = log.getLogger(DEFAULT_NAME_SPACE); -log.setDefaultLevel(log.levels.WARN); +log.setLevel(log.levels.WARN); /** * Drop-in replacement for console using {@link https://www.npmjs.com/package/loglevel|loglevel}. From 9d5c877df9e8d4488c635551c5bc44f82fa546fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Jaenisch?= Date: Wed, 24 Oct 2018 20:55:54 +0200 Subject: [PATCH 347/472] Set loglevel to DEBUG to remain current behaviour. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Jaenisch --- src/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logger.js b/src/logger.js index 9597df8ce..4b1a3a811 100644 --- a/src/logger.js +++ b/src/logger.js @@ -12,7 +12,7 @@ const log = require("loglevel"); // Part of #332 is introducing a logging library in the first place. const DEFAULT_NAME_SPACE = "matrix"; const logger = log.getLogger(DEFAULT_NAME_SPACE); -log.setLevel(log.levels.WARN); +log.setLevel(log.levels.DEBUG); /** * Drop-in replacement for console using {@link https://www.npmjs.com/package/loglevel|loglevel}. From b3c9570b0f3ffacc6c50f764a5a599f2d899637f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Jaenisch?= Date: Wed, 24 Oct 2018 20:56:29 +0200 Subject: [PATCH 348/472] Remove 'use strict'. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Jaenisch --- src/logger.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/logger.js b/src/logger.js index 4b1a3a811..51e4fd98f 100644 --- a/src/logger.js +++ b/src/logger.js @@ -1,4 +1,3 @@ -"use strict"; /** * @module logger */ From 8331c2f2673d9a6ee846350b3c18a22e2597bde2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Jaenisch?= Date: Wed, 24 Oct 2018 21:05:21 +0200 Subject: [PATCH 349/472] Use ES6 export instead of CommonJS. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Jaenisch --- src/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logger.js b/src/logger.js index 51e4fd98f..ef37b5b77 100644 --- a/src/logger.js +++ b/src/logger.js @@ -17,4 +17,4 @@ log.setLevel(log.levels.DEBUG); * Drop-in replacement for console using {@link https://www.npmjs.com/package/loglevel|loglevel}. * Can be tailored down to specific use cases if needed. */ -module.exports = logger; +export logger; From 3ce42a096be072fb84ae57eb62f72a6e5b003ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Jaenisch?= Date: Wed, 24 Oct 2018 21:06:04 +0200 Subject: [PATCH 350/472] Add Apache license banner. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Jaenisch --- src/logger.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/logger.js b/src/logger.js index ef37b5b77..cd5c20a8b 100644 --- a/src/logger.js +++ b/src/logger.js @@ -1,3 +1,19 @@ +/* +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 */ From 5b72509dac230099cd5760ab9d9e206cb9b1c405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Jaenisch?= Date: Wed, 24 Oct 2018 21:12:51 +0200 Subject: [PATCH 351/472] Fix broken build. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Jaenisch --- src/logger.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/logger.js b/src/logger.js index cd5c20a8b..6d22e5bad 100644 --- a/src/logger.js +++ b/src/logger.js @@ -27,10 +27,10 @@ const log = require("loglevel"); // Part of #332 is introducing a logging library in the first place. const DEFAULT_NAME_SPACE = "matrix"; const logger = log.getLogger(DEFAULT_NAME_SPACE); -log.setLevel(log.levels.DEBUG); +logger.setLevel(log.levels.DEBUG); /** * Drop-in replacement for console using {@link https://www.npmjs.com/package/loglevel|loglevel}. * Can be tailored down to specific use cases if needed. */ -export logger; +export default logger; From ae645ad9f0173639dfaf642928684849e4a16ec1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Jaenisch?= Date: Wed, 24 Oct 2018 21:52:50 +0200 Subject: [PATCH 352/472] Use Node.js module export, since ES6 export breaks build. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Jaenisch --- src/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logger.js b/src/logger.js index 6d22e5bad..ee77c2918 100644 --- a/src/logger.js +++ b/src/logger.js @@ -33,4 +33,4 @@ logger.setLevel(log.levels.DEBUG); * Drop-in replacement for console using {@link https://www.npmjs.com/package/loglevel|loglevel}. * Can be tailored down to specific use cases if needed. */ -export default logger; +module.exports = logger; From 6033b7b886e0d4190df520c57089fc8ceb5aff4e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 24 Oct 2018 16:36:00 -0600 Subject: [PATCH 353/472] Update request and browserify; Use request in the browser --- browser-index.js | 2 +- package.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/browser-index.js b/browser-index.js index 66a6036f2..32b62640c 100644 --- a/browser-index.js +++ b/browser-index.js @@ -1,5 +1,5 @@ var matrixcs = require("./lib/matrix"); -matrixcs.request(require("browser-request")); +matrixcs.request(require("request")); // just *accessing* indexedDB throws an exception in firefox with // indexeddb disabled. diff --git a/package.json b/package.json index 88f87f90b..ce88c7422 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "bluebird": "^3.5.0", "browser-request": "^0.3.3", "content-type": "^1.0.2", - "request": "^2.53.0" + "request": "^2.88.0" }, "devDependencies": { "babel-cli": "^6.18.0", @@ -65,7 +65,7 @@ "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-config-google": "^0.7.1", @@ -81,7 +81,7 @@ "source-map-support": "^0.4.11", "sourceify": "^0.1.0", "uglify-js": "^2.8.26", - "watchify": "^3.2.1" + "watchify": "^3.11.0" }, "browserify": { "transform": [ From 0c540ac8de7f36bd347ecbe3ad5993747cd24351 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 24 Oct 2018 16:36:12 -0600 Subject: [PATCH 354/472] Re-add the querystring options --- src/http-api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/http-api.js b/src/http-api.js index ca71d5668..65521df69 100644 --- a/src/http-api.js +++ b/src/http-api.js @@ -755,7 +755,7 @@ module.exports.MatrixHttpApi.prototype = { method: method, withCredentials: false, qs: queryParams, - //qsStringifyOptions: opts.qsStringifyOptions, + qsStringifyOptions: opts.qsStringifyOptions, useQuerystring: true, body: data, json: false, From 2cccb8b450eaebeebe3c0265e1e06da7679ed8c9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 24 Oct 2018 16:57:36 -0600 Subject: [PATCH 355/472] Install memfs because webpack is made of fail --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index ce88c7422..0c3fa3643 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "bluebird": "^3.5.0", "browser-request": "^0.3.3", "content-type": "^1.0.2", + "memfs": "^2.10.1", "request": "^2.88.0" }, "devDependencies": { From 2b752c0c02b0dbcd33d07dc685c5db29b83b7027 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 25 Oct 2018 14:29:25 +0100 Subject: [PATCH 356/472] 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) --- spec/TestClient.js | 1 + spec/integ/devicelist-integ-spec.js | 2 +- spec/integ/matrix-client-crypto.spec.js | 4 ++-- spec/integ/matrix-client-event-emitter.spec.js | 1 + spec/integ/matrix-client-event-timeline.spec.js | 1 + spec/integ/matrix-client-methods.spec.js | 1 + spec/integ/matrix-client-opts.spec.js | 1 + spec/integ/matrix-client-retrying.spec.js | 1 + spec/integ/matrix-client-room-timeline.spec.js | 1 + spec/integ/matrix-client-syncing.spec.js | 1 + spec/integ/megolm-integ.spec.js | 2 +- 11 files changed, 12 insertions(+), 4 deletions(-) diff --git a/spec/TestClient.js b/spec/TestClient.js index 1a7fa0816..cf1382bb6 100644 --- a/spec/TestClient.js +++ b/spec/TestClient.js @@ -105,6 +105,7 @@ TestClient.prototype.start = function() { */ TestClient.prototype.stop = function() { this.client.stopClient(); + return this.httpBackend.stop(); }; /** diff --git a/spec/integ/devicelist-integ-spec.js b/spec/integ/devicelist-integ-spec.js index f3f889deb..b671ce5bf 100644 --- a/spec/integ/devicelist-integ-spec.js +++ b/spec/integ/devicelist-integ-spec.js @@ -97,7 +97,7 @@ 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() { diff --git a/spec/integ/matrix-client-crypto.spec.js b/spec/integ/matrix-client-crypto.spec.js index 31a40aaf6..96bdd408e 100644 --- a/spec/integ/matrix-client-crypto.spec.js +++ b/spec/integ/matrix-client-crypto.spec.js @@ -410,10 +410,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() { diff --git a/spec/integ/matrix-client-event-emitter.spec.js b/spec/integ/matrix-client-event-emitter.spec.js index c494e8bc4..e2fc810af 100644 --- a/spec/integ/matrix-client-event-emitter.spec.js +++ b/spec/integ/matrix-client-event-emitter.spec.js @@ -30,6 +30,7 @@ describe("MatrixClient events", function() { afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); client.stopClient(); + return httpBackend.stop(); }); describe("emissions", function() { diff --git a/spec/integ/matrix-client-event-timeline.spec.js b/spec/integ/matrix-client-event-timeline.spec.js index 6fd3778be..2f1c9fa87 100644 --- a/spec/integ/matrix-client-event-timeline.spec.js +++ b/spec/integ/matrix-client-event-timeline.spec.js @@ -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) { diff --git a/spec/integ/matrix-client-methods.spec.js b/spec/integ/matrix-client-methods.spec.js index 5077a8ef4..e27a1a5b7 100644 --- a/spec/integ/matrix-client-methods.spec.js +++ b/spec/integ/matrix-client-methods.spec.js @@ -41,6 +41,7 @@ describe("MatrixClient", function() { afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); + return httpBackend.stop(); }); describe("uploadContent", function() { diff --git a/spec/integ/matrix-client-opts.spec.js b/spec/integ/matrix-client-opts.spec.js index 201ae7960..f2bc17e01 100644 --- a/spec/integ/matrix-client-opts.spec.js +++ b/spec/integ/matrix-client-opts.spec.js @@ -64,6 +64,7 @@ describe("MatrixClient opts", function() { afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); + return httpBackend.stop(); }); describe("without opts.store", function() { diff --git a/spec/integ/matrix-client-retrying.spec.js b/spec/integ/matrix-client-retrying.spec.js index af59e1ef6..b82294f2c 100644 --- a/spec/integ/matrix-client-retrying.spec.js +++ b/spec/integ/matrix-client-retrying.spec.js @@ -36,6 +36,7 @@ describe("MatrixClient retrying", function() { afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); + return httpBackend.stop(); }); xit("should retry according to MatrixScheduler.retryFn", function() { diff --git a/spec/integ/matrix-client-room-timeline.spec.js b/spec/integ/matrix-client-room-timeline.spec.js index fdc5ea097..5970ceb66 100644 --- a/spec/integ/matrix-client-room-timeline.spec.js +++ b/spec/integ/matrix-client-room-timeline.spec.js @@ -130,6 +130,7 @@ describe("MatrixClient room timelines", function() { afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); client.stopClient(); + return httpBackend.stop(); }); describe("local echo events", function() { diff --git a/spec/integ/matrix-client-syncing.spec.js b/spec/integ/matrix-client-syncing.spec.js index a74eea53a..6002436c4 100644 --- a/spec/integ/matrix-client-syncing.spec.js +++ b/spec/integ/matrix-client-syncing.spec.js @@ -38,6 +38,7 @@ describe("MatrixClient syncing", function() { afterEach(function() { httpBackend.verifyNoOutstandingExpectation(); client.stopClient(); + return httpBackend.stop(); }); describe("startClient", function() { diff --git a/spec/integ/megolm-integ.spec.js b/spec/integ/megolm-integ.spec.js index cb59ab5da..9a37e0b38 100644 --- a/spec/integ/megolm-integ.spec.js +++ b/spec/integ/megolm-integ.spec.js @@ -296,7 +296,7 @@ describe("megolm", function() { }); afterEach(function() { - aliceTestClient.stop(); + return aliceTestClient.stop(); }); it("Alice receives a megolm message", function() { From 997caad9855dc2918650682855079e9080ed4a66 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 25 Oct 2018 14:43:17 +0100 Subject: [PATCH 357/472] jsdoc --- spec/TestClient.js | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/TestClient.js b/spec/TestClient.js index cf1382bb6..1c08c2c7a 100644 --- a/spec/TestClient.js +++ b/spec/TestClient.js @@ -102,6 +102,7 @@ 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(); From 57173e438571985b5a489b0c7a19f24b45119ae3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 25 Oct 2018 15:01:05 +0100 Subject: [PATCH 358/472] Use mock-request 1.2.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 88f87f90b..0279aef32 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "istanbul": "^0.4.5", "jsdoc": "^3.5.5", "lolex": "^1.5.2", - "matrix-mock-request": "^1.2.0", + "matrix-mock-request": "^1.2.2", "mocha": "^5.2.0", "mocha-jenkins-reporter": "^0.4.0", "rimraf": "^2.5.4", From 88b39f4b672e06f1dd1edff1e6049f0e36b26d91 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 25 Oct 2018 19:00:03 +0100 Subject: [PATCH 359/472] 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. --- spec/unit/crypto/DeviceList.spec.js | 13 ++++++++++++- src/crypto/DeviceList.js | 6 ++++++ src/crypto/index.js | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/spec/unit/crypto/DeviceList.spec.js b/spec/unit/crypto/DeviceList.spec.js index 870e40bbc..5a442d81b 100644 --- a/spec/unit/crypto/DeviceList.spec.js +++ b/spec/unit/crypto/DeviceList.spec.js @@ -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() { diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index f86c9ff99..a9f62b138 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -146,6 +146,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 diff --git a/src/crypto/index.js b/src/crypto/index.js index 64b73164d..082f6d2c9 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -221,6 +221,7 @@ Crypto.prototype.start = function() { /** Stop background processes related to crypto */ Crypto.prototype.stop = function() { this._outgoingRoomKeyRequestManager.stop(); + this._deviceList.stop(); }; /** From b3bb99d76a8a0ca1984c15f73ce7d9c270351ac6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 25 Oct 2018 19:11:43 +0100 Subject: [PATCH 360/472] Stop client after backup tests --- spec/unit/crypto/backup.spec.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 3a3ed3a97..79f956fb0 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -441,6 +441,10 @@ describe("MegolmBackup", function() { return client.initCrypto(); }); + afterEach(function() { + client.stopClient(); + }); + it('can restore from backup', function() { client._http.authedRequest = function() { return Promise.resolve(KEY_BACKUP_DATA); From a6bf40d4e2e408ea937273be920e9def6e583c4c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 25 Oct 2018 19:21:29 +0100 Subject: [PATCH 361/472] We can always import these now --- spec/unit/crypto/backup.spec.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 79f956fb0..511f81998 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -29,13 +29,8 @@ 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 Olm = global.Olm; From 568ff5a3f576cf5b07b00c04fb9b47a8d82c07f6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 25 Oct 2018 14:36:14 -0600 Subject: [PATCH 362/472] Appease the linter --- src/client.js | 3 ++- src/http-api.js | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/client.js b/src/client.js index 1956b9225..77d62c6f0 100644 --- a/src/client.js +++ b/src/client.js @@ -968,7 +968,8 @@ MatrixClient.prototype.joinRoom = function(roomIdOrAlias, opts, callback) { } const path = utils.encodeUri("/join/$roomid", { $roomid: roomIdOrAlias}); - return self._http.authedRequest(undefined, "POST", path, queryString, data, reqOpts); + return self._http.authedRequest( + undefined, "POST", path, queryString, data, reqOpts); }).then(function(res) { const roomId = res.room_id; const syncApi = new SyncApi(self, self._clientOpts); diff --git a/src/http-api.js b/src/http-api.js index 65521df69..c29250d2a 100644 --- a/src/http-api.js +++ b/src/http-api.js @@ -668,9 +668,6 @@ module.exports.MatrixHttpApi.prototype = { * @param {function=} opts.bodyParser function to parse the body of the * response before passing it to the promise and callback. * - * @param (object=} opts.qsStringifyOptions options for stringifying the - * query string. - * * @return {module:client.Promise} a promise which resolves to either the * response object (if this.opts.onlyData is truthy), or the parsed * body. Rejects From 6b184363a161728b88f46608daeff4f2af90c6b2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 26 Oct 2018 11:16:20 +0100 Subject: [PATCH 363/472] loglevel should be a normal dependency rather than a dev dependency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6a4b6dd94..9354b525e 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "src" ], "dependencies": { + "loglevel": "1.6.1", "another-json": "^0.2.0", "babel-runtime": "^6.26.0", "bluebird": "^3.5.0", @@ -73,7 +74,6 @@ "expect": "^1.20.2", "istanbul": "^0.4.5", "jsdoc": "^3.5.5", - "loglevel": "1.6.1", "lolex": "^1.5.2", "matrix-mock-request": "^1.2.2", "mocha": "^5.2.0", From 4d0f6df89a3de763d9e198d528c19e6df625a68d Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 26 Oct 2018 11:23:05 +0100 Subject: [PATCH 364/472] alphabetical order --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9354b525e..fc081fe6d 100644 --- a/package.json +++ b/package.json @@ -52,12 +52,12 @@ "src" ], "dependencies": { - "loglevel": "1.6.1", "another-json": "^0.2.0", "babel-runtime": "^6.26.0", "bluebird": "^3.5.0", "browser-request": "^0.3.3", "content-type": "^1.0.2", + "loglevel": "1.6.1", "request": "^2.53.0" }, "devDependencies": { From 0e26247b5312aa386e36653c7d5da9f7b398e610 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 26 Oct 2018 12:08:55 +0100 Subject: [PATCH 365/472] Speed up time rather than increasing timeouts --- spec/unit/crypto/backup.spec.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 511f81998..64b85c787 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -122,6 +122,7 @@ describe("MegolmBackup", function() { describe("backup", function() { let mockBaseApis; + let realSetTimeout; beforeEach(function() { mockBaseApis = {}; @@ -135,6 +136,18 @@ describe("MegolmBackup", function() { }); 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() { @@ -175,7 +188,6 @@ describe("MegolmBackup", function() { }); it('sends backups to the server', function() { - this.timeout(12000); // eslint-disable-line no-invalid-this const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); const ibGroupSession = new Olm.InboundGroupSession(); @@ -281,7 +293,6 @@ describe("MegolmBackup", function() { }); it('retries when a backup fails', function() { - this.timeout(12000); // eslint-disable-line no-invalid-this const groupSession = new Olm.OutboundGroupSession(); groupSession.create(); const ibGroupSession = new Olm.InboundGroupSession(); From 563e6b3cdd29a6a8174a0eb65aff10c99d00afa1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 26 Oct 2018 13:23:37 +0100 Subject: [PATCH 366/472] Fix jsdoc --- src/crypto/store/indexeddb-crypto-store.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 0cfa86b5a..fb49ee1e8 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -437,7 +437,7 @@ export default class IndexedDBCryptoStore { /** * Unmark sessions as needing to be backed up. - * @param {[object]} sessions The sessions that need to be backed up. + * @param {Array} sessions The sessions that need to be backed up. * @returns {Promise} resolves when the sessions are unmarked */ unmarkSessionsNeedingBackup(sessions) { @@ -448,7 +448,7 @@ export default class IndexedDBCryptoStore { /** * Mark sessions as needing to be backed up. - * @param {[object]} sessions The sessions that need to be backed up. + * @param {Array} 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 */ From 3b2f2f922e45ad7eb9c6801f52ff1973c69159d2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 26 Oct 2018 18:49:05 +0100 Subject: [PATCH 367/472] Bump db version --- src/crypto/store/indexeddb-crypto-store-backend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index c27c48e3b..ac21e6f07 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -20,7 +20,7 @@ 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 From a76f0c7cb417946cba7a5e2a86a5d062c6e4f0a2 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 26 Oct 2018 22:06:33 -0500 Subject: [PATCH 368/472] Update babel-eslint to 8.1.1 Signed-off-by: Aaron Raimist --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f2ff9630..b718feeef 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ }, "devDependencies": { "babel-cli": "^6.18.0", - "babel-eslint": "^7.1.1", + "babel-eslint": "^8.1.1", "babel-plugin-transform-async-to-bluebird": "^1.1.1", "babel-plugin-transform-runtime": "^6.23.0", "babel-preset-es2015": "^6.18.0", From 4cbf9c7f47cc3cf4783e234512280707f9fa9b1e Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 29 Oct 2018 13:59:39 +0000 Subject: [PATCH 369/472] Prepare changelog for v0.12.1 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb0a47560..4295d74fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +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) From 68c6393eb2b658120ad832f0866ba70a997f650c Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 29 Oct 2018 13:59:39 +0000 Subject: [PATCH 370/472] v0.12.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 211c3c30b..8c9c9544f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.12.1-rc.1", + "version": "0.12.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From e51d2dd36abd7325c9358d0136a91b093e77a564 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 30 Oct 2018 11:45:19 +0000 Subject: [PATCH 371/472] 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 --- src/client.js | 2 -- src/crypto/index.js | 28 +++++++++++++++++----------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/client.js b/src/client.js index eb964650a..9bb259038 100644 --- a/src/client.js +++ b/src/client.js @@ -839,8 +839,6 @@ MatrixClient.prototype.enableKeyBackup = function(info) { this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); this.emit('keyBackupStatus', true); - - this._crypto._maybeSendKeyBackup(); }; /** diff --git a/src/crypto/index.js b/src/crypto/index.js index 655a801dc..7f2bfdb16 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -970,16 +970,21 @@ Crypto.prototype.importRoomKeys = function(keys) { ); }; -Crypto.prototype._maybeSendKeyBackup = async function() { +Crypto.prototype._maybeSendKeyBackup = async function(delay, retry) { + if (retry === undefined) retry = true; + if (!this._sendingBackups) { this._sendingBackups = true; try { - // wait between 0 and 10 seconds, to avoid backup requests from - // different clients hitting the server all at the same time when a - // new key is sent - await new Promise((resolve, reject) => { - setTimeout(resolve, Math.random() * 10000); - }); + if (delay === undefined) { + // by default, wait between 0 and 10 seconds, to avoid backup + // requests from different clients hitting the server all at + // the same time when a new key is sent + delay = Math.random() * 10000; + } + if (delay > 0) { + await Promise.delay(delay); + } let numFailures = 0; // number of consecutive failures while (1) { if (!this.backupKey) { @@ -1034,10 +1039,11 @@ Crypto.prototype._maybeSendKeyBackup = async function() { console.log("send failed", err); if (err.httpStatus === 400 || err.httpStatus === 403 - || err.httpStatus === 401) { + || err.httpStatus === 401 + || !retry) { // retrying probably won't help much, so we should give up // FIXME: disable backups? - return; + throw err; } } if (numFailures) { @@ -1076,7 +1082,7 @@ Crypto.prototype.backupGroupSession = async function( Crypto.prototype.backupAllGroupSessions = async function(version) { await this._cryptoStore.doTxn( 'readwrite', - [IndexedDBCryptoStore.STORE_SESSIONS, IndexedDBCryptoStore.STORE_BACKUP], + [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_BACKUP], (txn) => { this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => { if (session !== null) { @@ -1086,7 +1092,7 @@ Crypto.prototype.backupAllGroupSessions = async function(version) { }, ); - await this._maybeSendKeyBackup(); + await this._maybeSendKeyBackup(0, false); }; /* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307 From a2430dbc5387ed784e6f392c168c2ac5d3fa83ed Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 30 Oct 2018 12:29:44 +0000 Subject: [PATCH 372/472] Fix DeviceList index of users by identity key Was causing all keys to be send as unverified --- src/crypto/DeviceList.js | 45 ++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index e85cb7ef3..9416dea7a 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -142,7 +142,10 @@ export default class DeviceList { if (!userDevices.hasOwnProperty(device)) { continue; } - this._userByIdentityKey[userDevices[device].senderKey] = user; + const identityKey = userDevices[device].keys['curve25519:'+device]; + if (identityKey !== undefined) { + this._userByIdentityKey[identityKey] = user; + } } } }); @@ -442,12 +445,22 @@ export default class DeviceList { * @param {Object} devs New device info for user */ storeDevicesForUser(u, devs) { - this._devices[u] = devs; - for (const device in devs) { - if (!devs.hasOwnProperty(device)) { - continue; + // 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._userByIdentityKey[devs[device].senderKey] = u; + } + + this._devices[u] = devs; + + // add enw ones + for (const [deviceId, dev] of Object.entries(devs)) { + const identityKey = dev.keys['curve25519:'+deviceId]; + + this._userByIdentityKey[identityKey] = u; } this._dirty = true; } @@ -565,12 +578,22 @@ export default class DeviceList { * @param {Object} devices deviceId->{object} the new devices */ _setRawStoredDevicesForUser(userId, devices) { - this._devices[userId] = devices; - for (const device in devices) { - if (!devices.hasOwnProperty(device)) { - continue; + // 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._userByIdentityKey[devices[device].senderKey] = userId; + } + + 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; } } From 2814932845a26ce2b84ba3c82900f96de664cb80 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 30 Oct 2018 12:36:03 +0000 Subject: [PATCH 373/472] lint --- src/crypto/DeviceList.js | 6 +++--- src/crypto/index.js | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index 9416dea7a..fa1d1e18a 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -142,9 +142,9 @@ export default class DeviceList { if (!userDevices.hasOwnProperty(device)) { continue; } - const identityKey = userDevices[device].keys['curve25519:'+device]; - if (identityKey !== undefined) { - this._userByIdentityKey[identityKey] = user; + const idKey = userDevices[device].keys['curve25519:'+device]; + if (idKey !== undefined) { + this._userByIdentityKey[idKey] = user; } } } diff --git a/src/crypto/index.js b/src/crypto/index.js index 7f2bfdb16..3918ec862 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1082,7 +1082,10 @@ Crypto.prototype.backupGroupSession = async function( Crypto.prototype.backupAllGroupSessions = async function(version) { await this._cryptoStore.doTxn( 'readwrite', - [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_BACKUP], + [ + IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, + IndexedDBCryptoStore.STORE_BACKUP, + ], (txn) => { this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => { if (session !== null) { From ad279dc566bbba4b4f7f00c7371fd09bbc31753f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 30 Oct 2018 16:05:44 +0100 Subject: [PATCH 374/472] correctly check for crypto being present --- src/sync.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sync.js b/src/sync.js index 76122874b..1ce75ba82 100644 --- a/src/sync.js +++ b/src/sync.js @@ -516,7 +516,7 @@ SyncApi.prototype.sync = function() { console.warn("InvalidStoreError: store is not usable: stopping sync."); return; } - if (this.opts.lazyLoadMembers && this._crypto) { + if (this.opts.lazyLoadMembers && this.opts.crypto) { this.opts.crypto.enableLazyLoading(); } await this.client._storeClientOptions(); From f2881126cd35fd97e09299222eb0b5cbcc4ee938 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Oct 2018 13:59:29 -0600 Subject: [PATCH 375/472] 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 --- browser-index.js | 4 +++- package.json | 1 + src/http-api.js | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/browser-index.js b/browser-index.js index 32b62640c..c08249231 100644 --- a/browser-index.js +++ b/browser-index.js @@ -1,5 +1,7 @@ var matrixcs = require("./lib/matrix"); -matrixcs.request(require("request")); +var request = require("browser-request"); +request.enableConstructionOfQueryString = true; // note: this is long so we hopefully don't collide +matrixcs.request(request); // just *accessing* indexedDB throws an exception in firefox with // indexeddb disabled. diff --git a/package.json b/package.json index 5dfc84c94..1f421a003 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "content-type": "^1.0.2", "loglevel": "1.6.1", "memfs": "^2.10.1", + "query-string": "^6.2.0", "request": "^2.88.0" }, "devDependencies": { diff --git a/src/http-api.js b/src/http-api.js index c29250d2a..ab4b749ba 100644 --- a/src/http-api.js +++ b/src/http-api.js @@ -19,6 +19,7 @@ limitations under the License. * @module http-api */ import Promise from 'bluebird'; +const queryString = require('query-string'); const parseContentType = require('content-type').parse; const utils = require("./utils"); @@ -746,6 +747,9 @@ module.exports.MatrixHttpApi.prototype = { const reqPromise = defer.promise; try { + if (this.opts.request.enableConstructionOfQueryString) { + queryParams = queryString.stringify(queryParams, opts.qsStringifyOptions); + } req = this.opts.request( { uri: uri, From 68b65dd357b7071d4dad299ed48d9c277fc9904b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Oct 2018 14:12:41 -0600 Subject: [PATCH 376/472] Because uglify-js breaks everything --- browser-index.js | 2 +- src/http-api.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/browser-index.js b/browser-index.js index c08249231..259e02015 100644 --- a/browser-index.js +++ b/browser-index.js @@ -1,6 +1,6 @@ var matrixcs = require("./lib/matrix"); var request = require("browser-request"); -request.enableConstructionOfQueryString = true; // note: this is long so we hopefully don't collide +global.enableConstructionOfQueryString = true; // note: this is long so we hopefully don't collide matrixcs.request(request); // just *accessing* indexedDB throws an exception in firefox with diff --git a/src/http-api.js b/src/http-api.js index ab4b749ba..1c210073d 100644 --- a/src/http-api.js +++ b/src/http-api.js @@ -747,7 +747,7 @@ module.exports.MatrixHttpApi.prototype = { const reqPromise = defer.promise; try { - if (this.opts.request.enableConstructionOfQueryString) { + if (global && global.enableConstructionOfQueryString) { queryParams = queryString.stringify(queryParams, opts.qsStringifyOptions); } req = this.opts.request( From 054aac17aa8870157a6ea2d740fca9aef6ef2af7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Oct 2018 14:25:25 -0600 Subject: [PATCH 377/472] Just use the interface provided to us rather than hack in a flag --- browser-index.js | 16 +++++++++++++--- src/http-api.js | 4 ---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/browser-index.js b/browser-index.js index 259e02015..9ca2bf5f1 100644 --- a/browser-index.js +++ b/browser-index.js @@ -1,7 +1,17 @@ var matrixcs = require("./lib/matrix"); -var request = require("browser-request"); -global.enableConstructionOfQueryString = true; // note: this is long so we hopefully don't collide -matrixcs.request(request); +const request = require('browser-request'); +const queryString = require('query-string'); + +matrixcs.request((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. diff --git a/src/http-api.js b/src/http-api.js index 1c210073d..c29250d2a 100644 --- a/src/http-api.js +++ b/src/http-api.js @@ -19,7 +19,6 @@ limitations under the License. * @module http-api */ import Promise from 'bluebird'; -const queryString = require('query-string'); const parseContentType = require('content-type').parse; const utils = require("./utils"); @@ -747,9 +746,6 @@ module.exports.MatrixHttpApi.prototype = { const reqPromise = defer.promise; try { - if (global && global.enableConstructionOfQueryString) { - queryParams = queryString.stringify(queryParams, opts.qsStringifyOptions); - } req = this.opts.request( { uri: uri, From b57e858ad131c13d7789862ee310b5e6bda0cc9f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Oct 2018 14:27:51 -0600 Subject: [PATCH 378/472] We can't use arrow functions, apparently. --- browser-index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser-index.js b/browser-index.js index 9ca2bf5f1..e9576626f 100644 --- a/browser-index.js +++ b/browser-index.js @@ -2,7 +2,7 @@ var matrixcs = require("./lib/matrix"); const request = require('browser-request'); const queryString = require('query-string'); -matrixcs.request((opts, fn) => { +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 From 2fb29ae8fd14515b6c3d176e93a5697f09c52d78 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 30 Oct 2018 14:59:57 -0600 Subject: [PATCH 379/472] Use the right query string lib --- browser-index.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/browser-index.js b/browser-index.js index e9576626f..cc7e50fe8 100644 --- a/browser-index.js +++ b/browser-index.js @@ -1,6 +1,6 @@ var matrixcs = require("./lib/matrix"); const request = require('browser-request'); -const queryString = require('query-string'); +const queryString = require('qs'); matrixcs.request(function(opts, fn) { // We manually fix the query string for browser-request because diff --git a/package.json b/package.json index 1f421a003..26608e1e2 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "content-type": "^1.0.2", "loglevel": "1.6.1", "memfs": "^2.10.1", - "query-string": "^6.2.0", + "qs": "^6.5.2", "request": "^2.88.0" }, "devDependencies": { From 8ab84dee165d743bba049aa3c9e8c07ac6db3da3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Oct 2018 17:40:17 +0000 Subject: [PATCH 380/472] PR feedback 1/n --- spec/unit/crypto/backup.spec.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 64b85c787..5cf3b84bc 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -262,7 +262,8 @@ describe("MegolmBackup", function() { client._http.authedRequest = function( callback, method, path, queryParams, data, opts, ) { - expect(++numCalls <= 1); + ++numCalls; + expect(numCalls <= 1); if (numCalls >= 2) { // exit out of retry loop if there's something wrong reject(new Error("authedRequest called too many timmes")); @@ -285,10 +286,9 @@ describe("MegolmBackup", function() { groupSession.session_id(), groupSession.session_key(), ); - }) - .then(() => { - expect(numCalls).toBe(1); - }); + }).then(() => { + expect(numCalls).toBe(1); + }); }); }); @@ -367,7 +367,8 @@ describe("MegolmBackup", function() { client._http.authedRequest = function( callback, method, path, queryParams, data, opts, ) { - expect(++numCalls <= 2); + ++numCalls; + expect(numCalls <= 2); if (numCalls >= 3) { // exit out of retry loop if there's something wrong reject(new Error("authedRequest called too many timmes")); @@ -396,10 +397,9 @@ describe("MegolmBackup", function() { groupSession.session_id(), groupSession.session_key(), ); - }) - .then(() => { - expect(numCalls).toBe(2); - }); + }).then(() => { + expect(numCalls).toBe(2); + }); }); }); }); From c6ad0665b50c5947c78bf8180cb47d569b382682 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Oct 2018 18:03:40 +0000 Subject: [PATCH 381/472] factor out duplicated test code --- spec/unit/crypto/backup.spec.js | 80 +++++++++++++-------------------- 1 file changed, 30 insertions(+), 50 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 5cf3b84bc..c83949cf0 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -77,6 +77,34 @@ const KEY_BACKUP_DATA = { }, }; +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'); @@ -193,31 +221,7 @@ describe("MegolmBackup", function() { 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, - }); + const client = makeTestClient(sessionStore, cryptoStore); megolmDecryption = new MegolmDecryption({ userId: '@user:id', @@ -408,31 +412,7 @@ describe("MegolmBackup", function() { let client; beforeEach(function() { - 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)); - 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, - }); + client = makeTestClient(sessionStore, cryptoStore); megolmDecryption = new MegolmDecryption({ userId: '@user:id', From 2b46c560c7fb6e63d6df1001979fd413e405d2e5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Oct 2018 18:07:12 +0000 Subject: [PATCH 382/472] Add crypto. prefix to keyBackupStatus event --- src/client.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client.js b/src/client.js index 9bb259038..84c42ae18 100644 --- a/src/client.js +++ b/src/client.js @@ -838,7 +838,7 @@ MatrixClient.prototype.enableKeyBackup = function(info) { this._crypto.backupKey = new global.Olm.PkEncryption(); this._crypto.backupKey.set_recipient_key(info.auth_data.public_key); - this.emit('keyBackupStatus', true); + this.emit('crypto.keyBackupStatus', true); }; /** @@ -853,7 +853,7 @@ MatrixClient.prototype.disableKeyBackup = function() { if (this._crypto.backupKey) this._crypto.backupKey.free(); this._crypto.backupKey = null; - this.emit('keyBackupStatus', false); + this.emit('crypto.keyBackupStatus', false); }; /** @@ -4065,10 +4065,10 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED; /** * Fires whenever the status of e2e key backup changes, as returned by getKeyBackupEnabled() - * @event module:client~MatrixClient#"keyBackupStatus" + * @event module:client~MatrixClient#"crypto.keyBackupStatus" * @param {bool} enabled true if key backup has been enabled, otherwise false * @example - * matrixClient.on("keyBackupStatus", function(enabled){ + * matrixClient.on("crypto.keyBackupStatus", function(enabled){ * if (enabled) { * [...] * } From c5e7bedb37bdcd4fe6a659b6feace7d185046035 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Oct 2018 18:08:53 +0000 Subject: [PATCH 383/472] Conclusion: no, it shouldn't --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 84c42ae18..9f66b7898 100644 --- a/src/client.js +++ b/src/client.js @@ -897,7 +897,7 @@ MatrixClient.prototype.createKeyBackupVersion = function(info) { const data = { algorithm: info.algorithm, - auth_data: info.auth_data, // FIXME: should this be cloned? + auth_data: info.auth_data, }; return this._crypto._signObject(data.auth_data).then(() => { return this._http.authedRequest( From f5846b89ea0ee9487a0b87e27fd48cc34f843094 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Oct 2018 18:18:41 +0000 Subject: [PATCH 384/472] More modern loop syntax --- src/crypto/DeviceList.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index fa1d1e18a..417aa0dfb 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -133,15 +133,9 @@ export default class DeviceList { this._syncToken = deviceData ? deviceData.syncToken : null; } this._userByIdentityKey = {}; - for (const user in this._devices) { - if (!this._devices.hasOwnProperty(user)) { - continue; - } + for (const user of Object.keys(this._devices)) { const userDevices = this._devices[user]; - for (const device in userDevices) { - if (!userDevices.hasOwnProperty(device)) { - continue; - } + for (const device of Object.keys(userDevices)) { const idKey = userDevices[device].keys['curve25519:'+device]; if (idKey !== undefined) { this._userByIdentityKey[idKey] = user; From 6de213483cbe3ad23fbcd93be9e429378d4da755 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Oct 2018 18:31:56 +0000 Subject: [PATCH 385/472] Change getDeviceByIdentityKey() to just the 2 arg version --- src/crypto/DeviceList.js | 15 ++++----------- src/crypto/index.js | 2 +- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index 417aa0dfb..57bc74a4f 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -377,22 +377,15 @@ export default class DeviceList { /** * Find a device by curve25519 identity key * - * @param {string} userId owner of the device (optional) * @param {string} algorithm encryption algorithm * @param {string} senderKey curve25519 key to match * * @return {module:crypto/deviceinfo?} */ - getDeviceByIdentityKey(userId, algorithm, senderKey) { - if (arguments.length === 2) { - // if userId is omitted, shift the other arguments, and look up the - // userid - senderKey = algorithm; - algorithm = userId; - userId = this._userByIdentityKey[senderKey]; - if (!userId) { - return null; - } + getDeviceByIdentityKey(algorithm, senderKey) { + const userId = this._userByIdentityKey[senderKey]; + if (!userId) { + return null; } if ( diff --git a/src/crypto/index.js b/src/crypto/index.js index 3918ec862..97244f7b3 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -713,7 +713,7 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) { // identity key of the device which set up the Megolm session. const device = this._deviceList.getDeviceByIdentityKey( - event.getSender(), algorithm, senderKey, + algorithm, senderKey, ); if (device === null) { From 5c5ce0dfe306837ebc00ec1e017d4f2411e48c4f Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Oct 2018 18:32:48 +0000 Subject: [PATCH 386/472] Typo --- src/crypto/DeviceList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/DeviceList.js b/src/crypto/DeviceList.js index 57bc74a4f..751982b42 100644 --- a/src/crypto/DeviceList.js +++ b/src/crypto/DeviceList.js @@ -443,7 +443,7 @@ export default class DeviceList { this._devices[u] = devs; - // add enw ones + // add new ones for (const [deviceId, dev] of Object.entries(devs)) { const identityKey = dev.keys['curve25519:'+deviceId]; From db2897cf1edf2085f2c83ce887f9ef022fbe3dbd Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Oct 2018 18:33:31 +0000 Subject: [PATCH 387/472] Remove spurious interlopers --- src/crypto/OlmDevice.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 0b763fc1c..d7f182881 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -81,17 +81,6 @@ function OlmDevice(sessionStore, cryptoStore) { this.deviceEd25519Key = null; this._maxOneTimeKeys = null; - // track which of our other devices (if any) have cross-signed this device - // XXX: this should probably have a single source of truth in the /devices - // API store or whatever we use to track our self-signed devices. - this.crossSelfSigs = []; - - // track whether we have already suggested to the user that they should - // restore their keys from backup or by cross-signing the device. - // We use this to avoid repeatedly emitting the suggestion event. - // XXX: persist this somewhere! - this.suggestedKeyRestore = false; - // we don't bother stashing outboundgroupsessions in the sessionstore - // instead we keep them here. this._outboundGroupSessionStore = {}; From c77ecad9a5a44bdf3fc9f2e61b24d28d4eb50e42 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Oct 2018 18:34:49 +0000 Subject: [PATCH 388/472] clarify comment --- src/crypto/algorithms/megolm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index e36f46783..cf31e3056 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -850,7 +850,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { this._retryDecryption(senderKey, sessionId); }).then(() => { if (this._crypto.backupInfo) { - // don't wait for it to complete + // 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, From 7c0b910d7a6a77e44c6eeb0f88edf84f59bc83f0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Oct 2018 18:36:55 +0000 Subject: [PATCH 389/472] remove unnecessary isFinite check --- src/crypto/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 97244f7b3..62dab6185 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -206,7 +206,7 @@ Crypto.prototype._checkAndStartKeyBackup = async function() { backupInfo = await this._baseApis.getKeyBackupVersion(); } catch (e) { console.log("Error checking for active key backup", e); - if (Number.isFinite(e.httpStatus) && e.httpStatus / 100 === 4) { + if (e.httpStatus / 100 === 4) { // well that's told us. we won't try again. this._checkedForBackup = true; } From 63e9f794c7aefa7a26fb3b991c4735f6da8ea202 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Oct 2018 18:37:38 +0000 Subject: [PATCH 390/472] Remove unnecessary if --- src/crypto/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 62dab6185..024486271 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -982,9 +982,7 @@ Crypto.prototype._maybeSendKeyBackup = async function(delay, retry) { // the same time when a new key is sent delay = Math.random() * 10000; } - if (delay > 0) { - await Promise.delay(delay); - } + await Promise.delay(delay); let numFailures = 0; // number of consecutive failures while (1) { if (!this.backupKey) { From 2f219f83dbba1167465d85fb9f699117728219e6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Oct 2018 18:46:02 +0000 Subject: [PATCH 391/472] Catch exceptions from backupGroupSession() --- src/crypto/algorithms/megolm.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index cf31e3056..c13ca4dfa 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -269,7 +269,11 @@ MegolmEncryption.prototype._prepareNewSession = async function() { 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); @@ -855,7 +859,11 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { 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) => { logger.error(`Error handling m.room_key_event: ${e}`); @@ -984,7 +992,11 @@ MegolmDecryption.prototype.importRoomKey = function(session) { 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); From e89879d8a61acc0496c28d676e28138856268e9d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 31 Oct 2018 13:07:31 -0600 Subject: [PATCH 392/472] 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. --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 26608e1e2..4e1495ce0 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "browser-request": "^0.3.3", "content-type": "^1.0.2", "loglevel": "1.6.1", - "memfs": "^2.10.1", "qs": "^6.5.2", "request": "^2.88.0" }, From 5e9885946ff9b8dd2b1c340761f8740b5f2c2090 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Oct 2018 19:36:30 +0000 Subject: [PATCH 393/472] random double linebreak --- src/crypto/recoverykey.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/crypto/recoverykey.js b/src/crypto/recoverykey.js index bb85697e8..c9ff75f21 100644 --- a/src/crypto/recoverykey.js +++ b/src/crypto/recoverykey.js @@ -32,7 +32,6 @@ export function encodeRecoveryKey(key) { buf[buf.length - 1] = parity; const base58key = bs58.encode(buf); - return base58key.match(/.{1,4}/g).join(" "); } From 2af564324373fbf04a1baf8fa4726ee09bb3cf10 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Oct 2018 19:37:19 +0000 Subject: [PATCH 394/472] Clarify comment --- src/crypto/recoverykey.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/recoverykey.js b/src/crypto/recoverykey.js index c9ff75f21..d4d949f42 100644 --- a/src/crypto/recoverykey.js +++ b/src/crypto/recoverykey.js @@ -17,7 +17,7 @@ limitations under the License. import bs58 from 'bs58'; // picked arbitrarily but to try & avoid clashing with any bitcoin ones -// (also base58 encoded, albeit with a lot of hashing) +// (which are also base58 encoded, but bitcoin's involve a lot more hashing) const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01]; export function encodeRecoveryKey(key) { From c7a0c1402cc0f172829d8aae27793644a23ffce5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Oct 2018 19:39:07 +0000 Subject: [PATCH 395/472] refer to getAllEndToEndInboundGroupSessions for magic numbers --- src/crypto/store/localStorage-crypto-store.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index b88b8f9bd..65d94eda5 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -175,6 +175,7 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { 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( From 0477f354c9c244268b8a0b9e71158a347c6db6f2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Oct 2018 19:45:29 +0000 Subject: [PATCH 396/472] Fix key forwarded count It's exported in snake case --- src/crypto/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index 024486271..6fce09bcd 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1011,7 +1011,7 @@ Crypto.prototype._maybeSendKeyBackup = async function(delay, retry) { const encrypted = this.backupKey.encrypt(JSON.stringify(sessionData)); const forwardedCount = - (sessionData.forwardingCurve25519KeyChain || []).length; + (sessionData.forwarding_curve25519_key_chain || []).length; const device = this._deviceList.getDeviceByIdentityKey( olmlib.MEGOLM_ALGORITHM, session.senderKey, From 29d92d3e81aa7600c20c083014290b6d7c32f3e8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 31 Oct 2018 20:05:21 +0000 Subject: [PATCH 397/472] Lint --- spec/unit/crypto/backup.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index c83949cf0..bf9fe8edd 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -103,7 +103,7 @@ function makeTestClient(sessionStore, cryptoStore) { sessionStore: sessionStore, cryptoStore: cryptoStore, }); -}; +} describe("MegolmBackup", function() { if (!global.Olm) { From 28540ad50a8fd93db22aea585ca3441071e6c7f5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Nov 2018 12:59:38 +0000 Subject: [PATCH 398/472] 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) --- src/crypto/OlmDevice.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 5ce7438a7..851cca185 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -558,13 +558,20 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK * @return {Promise} 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 + sessionInfos.sort((a, b) => { + if (a.lastReceivedMessageTs !== b.lastReceivedMessageTs) { + return a.lastReceivedMessageTs - b.lastReceivedMessageTs; + } else { + if (a.sessionId === b.sessionId) return 0; + return a.sessionId < b.sessionId ? -1 : 1; + } + }); + return sessionInfos[sessionInfos.length - 1].sessionId; }; /** @@ -589,6 +596,7 @@ OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey) for (const sessionId of sessionIds) { this._unpickleSession(sessions[sessionId], (session) => { info.push({ + lastReceivedMessageTs: session.last_received_message_ts(), hasReceivedMessage: session.has_received_message(), sessionId: sessionId, }); @@ -649,6 +657,7 @@ OlmDevice.prototype.decryptMessage = async function( (txn) => { this._getSession(theirDeviceIdentityKey, sessionId, txn, (session) => { payloadString = session.decrypt(messageType, ciphertext); + session.set_last_received_message_ts(Date.now()); this._saveSession(theirDeviceIdentityKey, session, txn); }); }, From 37f0a9ad7b2602918d8d914faadd6bdfcea1f164 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Nov 2018 13:54:41 +0000 Subject: [PATCH 399/472] Try tests on node 10 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 67382e760..823517fc5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: node_js node_js: - - node # Latest stable version of nodejs. + - "10.11.0" script: - ./travis.sh From 2a6a67c6cc849644a1c03e590ba5dec0151afca1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 1 Nov 2018 17:08:43 +0000 Subject: [PATCH 400/472] Inbound session creation counts as a received message --- src/crypto/OlmDevice.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 851cca185..46f879ec2 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -509,6 +509,8 @@ OlmDevice.prototype.createInboundSession = async function( this._storeAccount(txn, account); const payloadString = session.decrypt(messageType, ciphertext); + // this counts as an received message + session.set_last_received_message_ts(Date.now()); this._saveSession(theirDeviceIdentityKey, session, txn); From c94382b46c586fbbb5e7c292ce42a649116a5117 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 2 Nov 2018 12:05:13 +0000 Subject: [PATCH 401/472] 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. --- spec/unit/crypto/algorithms/olm.spec.js | 90 +++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 spec/unit/crypto/algorithms/olm.spec.js diff --git a/spec/unit/crypto/algorithms/olm.spec.js b/spec/unit/crypto/algorithms/olm.spec.js new file mode 100644 index 000000000..e222e9694 --- /dev/null +++ b/spec/unit/crypto/algorithms/olm.spec.js @@ -0,0 +1,90 @@ +/* +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. +*/ + +try { + global.Olm = require('olm'); +} catch (e) { + console.warn("unable to run megolm tests: libolm not available"); +} + +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); + // expose for tests + olmDevice.__cryptoStore = 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("session reverted to a previous state can no longer decrypt messages", async function() { + const sid = await setupSession(aliceOlmDevice, bobOlmDevice); + //console.log("alice id key: " + aliceOlmDevice.deviceCurve25519Key); + //console.log("bob id key: " + bobOlmDevice.deviceCurve25519Key); + + 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"); + }); + }); +}); From ec8b3ae5157aa2f5cf809733ba45513888aedde0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 2 Nov 2018 12:33:46 +0000 Subject: [PATCH 402/472] Lint (and also name the test right) --- spec/unit/crypto/algorithms/olm.spec.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/spec/unit/crypto/algorithms/olm.spec.js b/spec/unit/crypto/algorithms/olm.spec.js index e222e9694..2dd6334cd 100644 --- a/spec/unit/crypto/algorithms/olm.spec.js +++ b/spec/unit/crypto/algorithms/olm.spec.js @@ -43,7 +43,9 @@ async function setupSession(initiator, opponent) { const keys = await opponent.getOneTimeKeys(); const firstKey = Object.values(keys['curve25519'])[0]; - const sid = await initiator.createOutboundSession(opponent.deviceCurve25519Key, firstKey); + const sid = await initiator.createOutboundSession( + opponent.deviceCurve25519Key, firstKey, + ); return sid; } @@ -68,7 +70,7 @@ describe("OlmDecryption", function() { }); describe('olm', function() { - it("session reverted to a previous state can no longer decrypt messages", async function() { + it("can decrypt messages", async function() { const sid = await setupSession(aliceOlmDevice, bobOlmDevice); //console.log("alice id key: " + aliceOlmDevice.deviceCurve25519Key); //console.log("bob id key: " + bobOlmDevice.deviceCurve25519Key); @@ -84,7 +86,9 @@ describe("OlmDecryption", function() { ciphertext.type, ciphertext.body, ); - expect(result.payload).toEqual("The olm or proteus is an aquatic salamander in the family Proteidae"); + expect(result.payload).toEqual( + "The olm or proteus is an aquatic salamander in the family Proteidae", + ); }); }); }); From 394e37f9ead40021779d4b351f63dd57fe0f0045 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 4 Nov 2018 19:43:18 +0000 Subject: [PATCH 403/472] Set access_token and user_id after login in with username and password. --- src/base-apis.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/base-apis.js b/src/base-apis.js index 6dc9169dd..d00ab64dc 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -277,7 +277,21 @@ MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback) return this.login("m.login.password", { user: user, password: password, - }, callback); + }, (error, response) => { + if (response && response.access_token) { + this._http.opts.accessToken = response.access_token; + } + + if (response && response.user_id) { + this.credentials = { + userId: response.user_id, + }; + } + + if(callback) { + callback(error, response); + } + }); }; /** From 3aabd63975fe760debbd8bb8727a7a7a98826222 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 4 Nov 2018 21:49:17 +0000 Subject: [PATCH 404/472] Add function to get currently joined rooms. --- src/base-apis.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/base-apis.js b/src/base-apis.js index 6dc9169dd..a0a842140 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -927,6 +927,14 @@ 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); +}; // Room Directory operations // ========================= From a0d51803ed46bb3bec03c85ac6fbcede603c5de8 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 00:08:04 +0000 Subject: [PATCH 405/472] Add function to get currently joined room members. --- src/base-apis.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/base-apis.js b/src/base-apis.js index a0a842140..38de1d693 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -936,6 +936,20 @@ MatrixBaseApis.prototype.getJoinedRooms = function() { 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 // ========================= From ff4125c11e809fb4b650f4df0fec5e042d14c07e Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 5 Nov 2018 09:39:46 +0000 Subject: [PATCH 406/472] Remove unused stuff & comments --- spec/unit/crypto/algorithms/olm.spec.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spec/unit/crypto/algorithms/olm.spec.js b/spec/unit/crypto/algorithms/olm.spec.js index 2dd6334cd..46fcff38b 100644 --- a/spec/unit/crypto/algorithms/olm.spec.js +++ b/spec/unit/crypto/algorithms/olm.spec.js @@ -33,8 +33,6 @@ function makeOlmDevice() { const sessionStore = new WebStorageSessionStore(mockStorage); const cryptoStore = new MemoryCryptoStore(mockStorage); const olmDevice = new OlmDevice(sessionStore, cryptoStore); - // expose for tests - olmDevice.__cryptoStore = cryptoStore; return olmDevice; } @@ -72,8 +70,6 @@ describe("OlmDecryption", function() { describe('olm', function() { it("can decrypt messages", async function() { const sid = await setupSession(aliceOlmDevice, bobOlmDevice); - //console.log("alice id key: " + aliceOlmDevice.deviceCurve25519Key); - //console.log("bob id key: " + bobOlmDevice.deviceCurve25519Key); const ciphertext = await aliceOlmDevice.encryptMessage( bobOlmDevice.deviceCurve25519Key, From 094598196a6e8166e2dd05ef6aaa0b01b7d0d9c1 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 16:02:30 +0000 Subject: [PATCH 407/472] Linting. --- src/base-apis.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base-apis.js b/src/base-apis.js index d00ab64dc..8836c59d8 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -288,7 +288,7 @@ MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback) }; } - if(callback) { + if (callback) { callback(error, response); } }); From b716e71784492b2c90555daae5f0648e5edf1922 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 16:40:11 +0000 Subject: [PATCH 408/472] Refactor code to base 'login' method. --- src/base-apis.js | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/base-apis.js b/src/base-apis.js index 8836c59d8..ad0f84b45 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -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, ); }; @@ -277,21 +289,7 @@ MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback) return this.login("m.login.password", { user: user, password: password, - }, (error, response) => { - if (response && response.access_token) { - this._http.opts.accessToken = response.access_token; - } - - if (response && response.user_id) { - this.credentials = { - userId: response.user_id, - }; - } - - if (callback) { - callback(error, response); - } - }); + }, callback); }; /** From f4abd7d027a921a7c34a3357b1da38f5fe907cde Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 17:06:39 +0000 Subject: [PATCH 409/472] Update CHANGELOG. --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4295d74fe..8c5b7ec7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +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. + 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) From 23dfeb13dfd56bd0814da927cbe8fea63ec0fd7d Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 17:17:22 +0000 Subject: [PATCH 410/472] Update CHANGELOG. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c5b7ec7d..666558997 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Changes in [0.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0. BREAKING CHANGE ---------------- - * `MatrixClient::login` now sets client `access_token` and `user_id` following successful login. + * `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) ================================================================================================== From d74ed508f99f0cf873f53c2f2f69cc1ba803adb6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 8 Nov 2018 19:09:28 +0000 Subject: [PATCH 411/472] 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 --- spec/unit/crypto.spec.js | 100 +++++++++++++++++- spec/unit/crypto/algorithms/megolm.spec.js | 93 +++++++++++++++- src/crypto/OlmDevice.js | 26 ++++- src/crypto/OutgoingRoomKeyRequestManager.js | 15 +++ src/crypto/algorithms/megolm.js | 87 ++++++++++++++- src/crypto/index.js | 94 +++++++++++++++- src/crypto/olmlib.js | 11 +- .../store/indexeddb-crypto-store-backend.js | 36 +++++++ src/crypto/store/indexeddb-crypto-store.js | 18 ++++ src/crypto/store/memory-crypto-store.js | 13 +++ 10 files changed, 476 insertions(+), 17 deletions(-) diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js index ee06ef369..47d7d2d67 100644 --- a/spec/unit/crypto.spec.js +++ b/spec/unit/crypto.spec.js @@ -1,9 +1,13 @@ - -"use strict"; import 'source-map-support/register'; 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'; + +const EventEmitter = require("events").EventEmitter; + const sdk = require("../.."); const Olm = global.Olm; @@ -20,4 +24,96 @@ describe("Crypto", function() { it("Crypto exposes the correct olm library version", function() { 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(), + }; + 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'), + }); + + console.log("waiting"); + await prom; + console.log("done"); + }); + }); }); diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index 6c777859e..641adb19c 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -18,6 +18,7 @@ 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'; @@ -34,9 +35,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 = {}; @@ -66,7 +69,6 @@ describe("MegolmDecryption", function() { describe('receives some keys:', function() { let groupSession; beforeEach(async function() { - await Olm.init(); groupSession = new global.Olm.OutboundGroupSession(); groupSession.create(); @@ -263,5 +265,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); + }); }); }); diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 46f879ec2..f9c907c78 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -461,6 +461,12 @@ OlmDevice.prototype.createOutboundSession = async function( session.create_outbound(account, theirIdentityKey, theirOneTimeKey); newSessionId = session.session_id(); this._storeAccount(txn, account); + // 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 (storing the creation time separately would + // make the pickle longer and would not be useful otherwise). + session.set_last_received_message_ts(Date.now()); + this._saveSession(theirIdentityKey, session, txn); } finally { session.free(); @@ -725,7 +731,7 @@ 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); } @@ -1059,6 +1065,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, @@ -1066,9 +1074,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( @@ -1079,14 +1090,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, diff --git a/src/crypto/OutgoingRoomKeyRequestManager.js b/src/crypto/OutgoingRoomKeyRequestManager.js index 4c9b7cbf5..bfde6019b 100644 --- a/src/crypto/OutgoingRoomKeyRequestManager.js +++ b/src/crypto/OutgoingRoomKeyRequestManager.js @@ -244,6 +244,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() { diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index d8d8fd8f2..c9792b38e 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -144,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; @@ -195,6 +200,7 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) { if (!session) { 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 @@ -409,8 +415,87 @@ 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 + const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId]; + + // 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} devicesByUser diff --git a/src/crypto/index.js b/src/crypto/index.js index 082f6d2c9..54c6bb16a 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1133,6 +1133,8 @@ Crypto.prototype._onToDeviceEvent = function(event) { this._onRoomKeyEvent(event); } else if (event.getType() == "m.room_key_request") { this._onRoomKeyRequestEvent(event); + } else if (event.getContent().msgtype === "m.bad.encrypted") { + this._onToDeviceBadEncrypted(event); } else if (event.isBeingDecrypted()) { // once the event has been decrypted, try again event.once('Event.decrypted', (ev) => { @@ -1162,6 +1164,74 @@ Crypto.prototype._onRoomKeyEvent = function(event) { alg.onRoomKeyEvent(event); }; +/** + * Handle a toDevice event that couldn't be decrypted + * + * @private + * @param {module:models/event.MatrixEvent} event undecryptable event + */ +Crypto.prototype._onToDeviceBadEncrypted = async function(event) { + const content = event.getWireContent(); + const sender = event.getSender(); + const algorithm = content.algorithm; + const deviceKey = content.sender_key; + + if (sender === undefined || deviceKey === undefined || deviceKey === undefined) { + return; + } + + // establish a new olm session with this device since we're failing to decrypt messages + // on a current session. + // Note that an undecryptable message from another device could easily be spoofed - + // is there anything we can do to mitigate this? + const device = this._deviceList.getDeviceByIdentityKey(sender, algorithm, deviceKey); + const devicesByUser = {}; + devicesByUser[sender] = [device]; + await olmlib.ensureOlmSessionsForDevices( + this._olmDevice, this._baseApis, devicesByUser, true, + ); + + // Now send a blank message on that session so the other side knows about it. + // (The keyshare request is sent in the clear so that won't do) + // We send this first such that, as long as the toDevice messages arrive in the + // same order we sent them, the other end will get this first, set up the new session, + // then get the keyshare request and send the key over this new session (because it + // it the session it has most recently received a message on). + const encryptedContent = { + algorithm: olmlib.OLM_ALGORITHM, + sender_key: this._olmDevice.deviceCurve25519Key, + ciphertext: {}, + }; + await olmlib.encryptMessageForDevice( + encryptedContent.ciphertext, + this._userId, + this._deviceId, + this._olmDevice, + sender, + device, + {type: "m.dummy"}, + ); + + await this._baseApis.sendToDevice("m.room.encrypted", { + [sender]: { + [device.deviceId]: encryptedContent, + }, + }); + + + // Most of the time this probably won't be necessary since we'll have queued up a key request when + // we failed to decrypt the message and will be waiting a bit for the key to arrive before sending + // it. This won't always be the case though so we need to re-send any that have already been sent + // to avoid races. + const requestsToResend = + await this._outgoingRoomKeyRequestManager.getOutgoingSentRoomKeyRequest( + sender, device.deviceId, + ); + for (const keyReq of requestsToResend) { + this.cancelRoomKeyRequest(keyReq.requestBody, true); + } +}; + /** * Handle a change in the membership state of a member of a room * @@ -1287,9 +1357,27 @@ Crypto.prototype._processReceivedRoomKeyRequest = async function(req) { ` for ${roomId} / ${body.session_id} (id ${req.requestId})`); if (userId !== this._userId) { - // TODO: determine if we sent this device the keys already: in - // which case we can do so again. - logger.log("Ignoring room key request from other user for now"); + if (!this._roomEncryptors[roomId]) { + logger.debug(`room key request for unencrypted room ${roomId}`); + return; + } + const encryptor = this._roomEncryptors[roomId]; + const device = this._deviceList.getStoredDevice(userId, deviceId); + if (!device) { + logger.debug(`Ignoring keyshare for unknown device ${userId}:${deviceId}`); + return; + } + + try { + await encryptor.reshareKeyWithDevice( + body.sender_key, body.session_id, userId, device, + ); + } catch (e) { + logger.warn( + "Failed to re-share keys for session " + body.session_id + + " with device " + userId + ":" + device.deviceId, e, + ); + } return; } diff --git a/src/crypto/olmlib.js b/src/crypto/olmlib.js index 49ec4c20e..75b8abb95 100644 --- a/src/crypto/olmlib.js +++ b/src/crypto/olmlib.js @@ -116,14 +116,17 @@ module.exports.encryptMessageForDevice = async function( * @param {module:base-apis~MatrixBaseApis} baseApis * * @param {object} 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], ... @@ -141,7 +144,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] = { @@ -177,7 +180,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; } diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index 96bbec68e..09841d2b7 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -206,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 diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index c9210da23..468d90a51 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -207,6 +207,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} 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 diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 4c2baf9e1..81bee95ba 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -145,6 +145,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 From c9a79bf32e86dae7fb060851b76276c9468d3e49 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 8 Nov 2018 16:46:03 -0700 Subject: [PATCH 412/472] Prevent messages from being sent if other messages have failed to send Fixes https://github.com/vector-im/riot-web/issues/5408 --- src/client.js | 7 +++++++ src/models/room.js | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/src/client.js b/src/client.js index d8f0d67ca..64bbf5082 100644 --- a/src/client.js +++ b/src/client.js @@ -1186,6 +1186,13 @@ MatrixClient.prototype.sendEvent = function(roomId, eventType, content, txnId, room.addPendingEvent(localEvent, txnId); } + // addPendingEvent can change the state to NOT_SENT if it believes + // that there's other events that have failed. We won't bother to + // try sending the event if the state has changed as such. + if (localEvent.status === EventStatus.NOT_SENT) { + return Promise.reject(new Error("Event blocked by other events not yet sent")); + } + return _sendEvent(this, room, localEvent, callback); }; diff --git a/src/models/room.js b/src/models/room.js index 4b685c878..6f05b48aa 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -999,6 +999,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 new event's status as " + EventStatus.NOT_SENT + " due to other similar messages"); + event.status = EventStatus.NOT_SENT; + } this._pendingEventList.push(event); } else { for (let i = 0; i < this._timelineSets.length; i++) { From 16db970558499df946568413921485d06d2d9966 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 8 Nov 2018 16:51:21 -0700 Subject: [PATCH 413/472] Appease the linter --- src/models/room.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/room.js b/src/models/room.js index 6f05b48aa..c58b4a632 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -999,8 +999,8 @@ 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 new event's status as " + EventStatus.NOT_SENT + " due to other similar messages"); + 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); From fed67192bc802d885b3a5e520a28f354417d1e36 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 8 Nov 2018 17:17:23 -0700 Subject: [PATCH 414/472] Fix test: Actually resend the event after unknown devices are found This test didn't actually test that resending would work, despite its name. --- spec/integ/megolm-integ.spec.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spec/integ/megolm-integ.spec.js b/spec/integ/megolm-integ.spec.js index 9a37e0b38..b302856cc 100644 --- a/spec/integ/megolm-integ.spec.js +++ b/spec/integ/megolm-integ.spec.js @@ -817,8 +817,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({ From 231fde219c3bc96427a7b4b4e343dc2a89892191 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 12 Nov 2018 18:10:11 +0000 Subject: [PATCH 415/472] Store last received message ts on olm session --- src/crypto/OlmDevice.js | 75 +++++++++++-------- .../store/indexeddb-crypto-store-backend.js | 19 ++++- src/crypto/store/indexeddb-crypto-store.js | 16 ++-- src/crypto/store/localStorage-crypto-store.js | 20 ++++- src/crypto/store/memory-crypto-store.js | 4 +- 5 files changed, 88 insertions(+), 46 deletions(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index f9c907c78..1b317e7ab 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -295,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); }, ); }; @@ -306,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) { +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(); } @@ -324,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, ); }; @@ -461,13 +466,15 @@ OlmDevice.prototype.createOutboundSession = async function( session.create_outbound(account, theirIdentityKey, theirOneTimeKey); newSessionId = session.session_id(); this._storeAccount(txn, account); - // 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 (storing the creation time separately would - // make the pickle longer and would not be useful otherwise). - session.set_last_received_message_ts(Date.now()); - - 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 (storing the creation time separately would + // make the pickle longer and would not be useful otherwise). + lastReceivedMessageTs: Date.now(), + }; + this._saveSession(theirIdentityKey, sessionInfo, txn); } finally { session.free(); } @@ -515,10 +522,14 @@ OlmDevice.prototype.createInboundSession = async function( this._storeAccount(txn, account); const payloadString = session.decrypt(messageType, ciphertext); - // this counts as an received message - session.set_last_received_message_ts(Date.now()); - this._saveSession(theirDeviceIdentityKey, session, txn); + const sessionInfo = { + session, + // this counts as an received message: set last received message time + // to now + lastReceivedMessageTs: Date.now(), + }; + this._saveSession(theirDeviceIdentityKey, sessionInfo, txn); result = { payload: payloadString, @@ -602,10 +613,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({ - lastReceivedMessageTs: session.last_received_message_ts(), - hasReceivedMessage: session.has_received_message(), + lastReceivedMessageTs: sessInfo.lastReceivedMessageTs, + hasReceivedMessage: sessInfo.session.has_received_message(), sessionId: sessionId, }); }); @@ -636,9 +647,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); }); }, ); @@ -663,10 +674,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); - session.set_last_received_message_ts(Date.now()); - 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); }); }, ); @@ -696,8 +707,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); }); }, ); diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index 09841d2b7..e3fb16d19 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -350,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 { @@ -368,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); } @@ -378,9 +384,14 @@ export class Backend { }; } - storeEndToEndSession(deviceKey, sessionId, session, txn) { + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { const objectStore = txn.objectStore("sessions"); - objectStore.put({deviceKey, sessionId, session}); + objectStore.put({ + deviceKey, + sessionId, + session: sessionInfo.session, + lastReceivedMessageTs: sessionInfo.lastReceivedMessageTs, + }); } // Inbound group sessions diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 468d90a51..3a7d6d1d6 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -302,7 +302,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); @@ -314,7 +317,10 @@ 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); @@ -324,12 +330,12 @@ export default class IndexedDBCryptoStore { * 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, ); } diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index ed0b7ede4..a47861b53 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -67,7 +67,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) { @@ -79,9 +93,9 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { func(this._getEndToEndSessions(deviceKey) || {}); } - storeEndToEndSession(deviceKey, sessionId, session, txn) { + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { const sessions = this._getEndToEndSessions(deviceKey) || {}; - sessions[sessionId] = session; + sessions[sessionId] = sessionInfo; setJsonItem( this.store, keyEndToEndSessions(deviceKey), sessions, ); diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 81bee95ba..56b541391 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -247,13 +247,13 @@ export default class MemoryCryptoStore { func(this._sessions[deviceKey] || {}); } - storeEndToEndSession(deviceKey, sessionId, session, txn) { + 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 From fcadf6ec4a052ba9f1a05fd26f23cadedb056b0c Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 12 Nov 2018 18:10:11 +0000 Subject: [PATCH 416/472] Store last received message ts on olm session --- src/crypto/OlmDevice.js | 69 ++++++++++++------- .../store/indexeddb-crypto-store-backend.js | 19 +++-- src/crypto/store/indexeddb-crypto-store.js | 16 +++-- src/crypto/store/localStorage-crypto-store.js | 20 +++++- src/crypto/store/memory-crypto-store.js | 4 +- 5 files changed, 88 insertions(+), 40 deletions(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 46f879ec2..2c195fef3 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -295,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); }, ); }; @@ -306,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) { +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(); } @@ -324,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, ); }; @@ -461,7 +466,15 @@ OlmDevice.prototype.createOutboundSession = async function( 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 (storing the creation time separately would + // make the pickle longer and would not be useful otherwise). + lastReceivedMessageTs: Date.now(), + }; + this._saveSession(theirIdentityKey, sessionInfo, txn); } finally { session.free(); } @@ -509,10 +522,14 @@ OlmDevice.prototype.createInboundSession = async function( this._storeAccount(txn, account); const payloadString = session.decrypt(messageType, ciphertext); - // this counts as an received message - session.set_last_received_message_ts(Date.now()); - this._saveSession(theirDeviceIdentityKey, session, txn); + const sessionInfo = { + session, + // this counts as an received message: set last received message time + // to now + lastReceivedMessageTs: Date.now(), + }; + this._saveSession(theirDeviceIdentityKey, sessionInfo, txn); result = { payload: payloadString, @@ -596,10 +613,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({ - lastReceivedMessageTs: session.last_received_message_ts(), - hasReceivedMessage: session.has_received_message(), + lastReceivedMessageTs: sessInfo.lastReceivedMessageTs, + hasReceivedMessage: sessInfo.session.has_received_message(), sessionId: sessionId, }); }); @@ -630,9 +647,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,10 +674,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); - session.set_last_received_message_ts(Date.now()); - 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); }); }, ); @@ -690,8 +707,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); }); }, ); diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index 96bbec68e..ba15e6673 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -314,7 +314,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 { @@ -332,7 +335,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); } @@ -342,9 +348,14 @@ export class Backend { }; } - storeEndToEndSession(deviceKey, sessionId, session, txn) { + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { const objectStore = txn.objectStore("sessions"); - objectStore.put({deviceKey, sessionId, session}); + objectStore.put({ + deviceKey, + sessionId, + session: sessionInfo.session, + lastReceivedMessageTs: sessionInfo.lastReceivedMessageTs, + }); } // Inbound group sessions diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index c9210da23..a4d7b48c0 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -284,7 +284,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); @@ -296,7 +299,10 @@ 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); @@ -306,12 +312,12 @@ export default class IndexedDBCryptoStore { * 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, ); } diff --git a/src/crypto/store/localStorage-crypto-store.js b/src/crypto/store/localStorage-crypto-store.js index ed0b7ede4..a47861b53 100644 --- a/src/crypto/store/localStorage-crypto-store.js +++ b/src/crypto/store/localStorage-crypto-store.js @@ -67,7 +67,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) { @@ -79,9 +93,9 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore { func(this._getEndToEndSessions(deviceKey) || {}); } - storeEndToEndSession(deviceKey, sessionId, session, txn) { + storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) { const sessions = this._getEndToEndSessions(deviceKey) || {}; - sessions[sessionId] = session; + sessions[sessionId] = sessionInfo; setJsonItem( this.store, keyEndToEndSessions(deviceKey), sessions, ); diff --git a/src/crypto/store/memory-crypto-store.js b/src/crypto/store/memory-crypto-store.js index 4c2baf9e1..8c8543c18 100644 --- a/src/crypto/store/memory-crypto-store.js +++ b/src/crypto/store/memory-crypto-store.js @@ -234,13 +234,13 @@ export default class MemoryCryptoStore { func(this._sessions[deviceKey] || {}); } - storeEndToEndSession(deviceKey, sessionId, session, txn) { + 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 From e17a39d446e0acd389e49110d94044ced45da88d Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Nov 2018 12:10:26 +0000 Subject: [PATCH 417/472] PR feedback --- src/crypto/OlmDevice.js | 24 ++++++++++--------- .../store/indexeddb-crypto-store-backend.js | 4 ++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 2c195fef3..c87ef9525 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -470,8 +470,7 @@ OlmDevice.prototype.createOutboundSession = async function( 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 (storing the creation time separately would - // make the pickle longer and would not be useful otherwise). + // this session lastReceivedMessageTs: Date.now(), }; this._saveSession(theirIdentityKey, sessionInfo, txn); @@ -525,7 +524,7 @@ OlmDevice.prototype.createInboundSession = async function( const sessionInfo = { session, - // this counts as an received message: set last received message time + // this counts as a received message: set last received message time // to now lastReceivedMessageTs: Date.now(), }; @@ -582,15 +581,18 @@ OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKe return null; } // Use the session that has most recently received a message - sessionInfos.sort((a, b) => { - if (a.lastReceivedMessageTs !== b.lastReceivedMessageTs) { - return a.lastReceivedMessageTs - b.lastReceivedMessageTs; - } else { - if (a.sessionId === b.sessionId) return 0; - return a.sessionId < b.sessionId ? -1 : 1; + let idxOfMin = 0; + for (let i = 1; i < sessionInfos.length; i++) { + if ( + sessionInfos[i].lastReceivedMessageTs < sessionInfos[idxOfMin].lastReceiveMessageTs || ( + sessionInfos[i].lastReceivedMessageTs === sessionInfos[idxOfMin].lastReceiveMessageTs && + sessionInfos[i].sessionId < sessinInfos[idxOfMin].sessionId + ) + ) { + idxOfMin = i; } - }); - return sessionInfos[sessionInfos.length - 1].sessionId; + } + return sessionInfos[idxOfMin].sessionId; }; /** diff --git a/src/crypto/store/indexeddb-crypto-store-backend.js b/src/crypto/store/indexeddb-crypto-store-backend.js index ba15e6673..0f31584cd 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.js +++ b/src/crypto/store/indexeddb-crypto-store-backend.js @@ -316,7 +316,7 @@ export class Backend { if (cursor) { results[cursor.value.sessionId] = { session: cursor.value.session, - lastReceivedMessagets: cursor.value.lastReceivedMessageTs, + lastReceivedMessageTs: cursor.value.lastReceivedMessageTs, }; cursor.continue(); } else { @@ -337,7 +337,7 @@ export class Backend { if (getReq.result) { func({ session: getReq.result.session, - lastReceivedMessagets: getReq.result.lastReceivedMessageTs, + lastReceivedMessageTs: getReq.result.lastReceivedMessageTs, }); } else { func(null); From 6c413bba4854986a8bbec98ff74918f93dbfdf00 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Nov 2018 12:15:33 +0000 Subject: [PATCH 418/472] Typo + exit if session was never shared --- src/crypto/algorithms/megolm.js | 8 ++++++++ src/crypto/index.js | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index c9792b38e..b4a78334a 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -433,7 +433,15 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function( } // 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. diff --git a/src/crypto/index.js b/src/crypto/index.js index 54c6bb16a..7c0ac003a 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1196,7 +1196,7 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) { // We send this first such that, as long as the toDevice messages arrive in the // same order we sent them, the other end will get this first, set up the new session, // then get the keyshare request and send the key over this new session (because it - // it the session it has most recently received a message on). + // is the session it has most recently received a message on). const encryptedContent = { algorithm: olmlib.OLM_ALGORITHM, sender_key: this._olmDevice.deviceCurve25519Key, From 379f290b8bfebda2f990b212e5f8a304b3b0b228 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 13 Nov 2018 14:49:56 +0000 Subject: [PATCH 419/472] Add package-lock.json to force base-x to version 3.0.4 because 3.0.5 breaks the build by exporting ES6. --- package-lock.json | 6856 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 6856 insertions(+) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..b56c65f2f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6856 @@ +{ + "name": "matrix-js-sdk", + "version": "0.12.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "accessory": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/accessory/-/accessory-1.1.0.tgz", + "integrity": "sha1-eDPpg5oy3tdtJgIfNqQXB6Ug9ZM=", + "dev": true, + "requires": { + "ap": "~0.2.0", + "balanced-match": "~0.2.0", + "dot-parts": "~1.0.0" + }, + "dependencies": { + "balanced-match": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.2.1.tgz", + "integrity": "sha1-e8ZYtL7WHu5CStdPdfXD4sTfPMc=", + "dev": true + } + } + }, + "acorn": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.4.tgz", + "integrity": "sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg==", + "dev": true + }, + "acorn-dynamic-import": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", + "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==", + "dev": true + }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "^3.0.4" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "http://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, + "acorn-node": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.6.2.tgz", + "integrity": "sha512-rIhNEZuNI8ibQcL7ANm/mGyPukIaZsRNX9psFNQURyJW0nu6k8wjSDld20z6v2mDBWqX13pIEnk9gGZJHIlEXg==", + "dev": true, + "requires": { + "acorn": "^6.0.2", + "acorn-dynamic-import": "^4.0.0", + "acorn-walk": "^6.1.0", + "xtend": "^4.0.1" + } + }, + "acorn-walk": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", + "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", + "dev": true + }, + "ajv": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", + "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", + "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", + "dev": true + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true, + "optional": true + }, + "another-json": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/another-json/-/another-json-0.2.0.tgz", + "integrity": "sha1-tfQBnJc7bdXGUGotk0acttMq7tw=" + }, + "ansi-escapes": { + "version": "1.4.0", + "resolved": "http://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "anymatch": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", + "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", + "dev": true, + "requires": { + "micromatch": "^2.1.5", + "normalize-path": "^2.0.0" + } + }, + "ap": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ap/-/ap-0.2.0.tgz", + "integrity": "sha1-rglCYAspkS8NKxTsYMRejzMLYRA=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-filter": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", + "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", + "dev": true + }, + "array-map": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", + "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", + "dev": true + }, + "array-reduce": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", + "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", + "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", + "dev": true, + "requires": { + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", + "dev": true + }, + "util": { + "version": "0.10.3", + "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "dev": true, + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "async-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz", + "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "babel-cli": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-cli/-/babel-cli-6.26.0.tgz", + "integrity": "sha1-UCq1SHTX24itALiHoGODzgPQAvE=", + "dev": true, + "requires": { + "babel-core": "^6.26.0", + "babel-polyfill": "^6.26.0", + "babel-register": "^6.26.0", + "babel-runtime": "^6.26.0", + "chokidar": "^1.6.1", + "commander": "^2.11.0", + "convert-source-map": "^1.5.0", + "fs-readdir-recursive": "^1.0.0", + "glob": "^7.1.2", + "lodash": "^4.17.4", + "output-file-sync": "^1.1.2", + "path-is-absolute": "^1.0.1", + "slash": "^1.0.0", + "source-map": "^0.5.6", + "v8flags": "^2.1.1" + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "babel-core": { + "version": "6.26.3", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.3.tgz", + "integrity": "sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA==", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-generator": "^6.26.0", + "babel-helpers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-register": "^6.26.0", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "convert-source-map": "^1.5.1", + "debug": "^2.6.9", + "json5": "^0.5.1", + "lodash": "^4.17.4", + "minimatch": "^3.0.4", + "path-is-absolute": "^1.0.1", + "private": "^0.1.8", + "slash": "^1.0.0", + "source-map": "^0.5.7" + } + }, + "babel-eslint": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-7.2.3.tgz", + "integrity": "sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc=", + "dev": true, + "requires": { + "babel-code-frame": "^6.22.0", + "babel-traverse": "^6.23.1", + "babel-types": "^6.23.0", + "babylon": "^6.17.0" + } + }, + "babel-generator": { + "version": "6.26.1", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz", + "integrity": "sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA==", + "dev": true, + "requires": { + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "detect-indent": "^4.0.0", + "jsesc": "^1.3.0", + "lodash": "^4.17.4", + "source-map": "^0.5.7", + "trim-right": "^1.0.1" + } + }, + "babel-helper-call-delegate": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz", + "integrity": "sha1-7Oaqzdx25Bw0YfiL/Fdb0Nqi340=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-define-map": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz", + "integrity": "sha1-pfVtq0GiX5fstJjH66ypgZ+Vvl8=", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-helper-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz", + "integrity": "sha1-00dbjAPtmCQqJbSDUasYOZ01gKk=", + "dev": true, + "requires": { + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helper-get-function-arity": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz", + "integrity": "sha1-j3eCqpNAfEHTqlCQj4mwMbG2hT0=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-hoist-variables": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz", + "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-optimise-call-expression": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz", + "integrity": "sha1-96E0J7qfc/j0+pk8VKl4gtEkQlc=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-helper-regex": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz", + "integrity": "sha1-MlxZ+QL4LyS3T6zu0DY5VPZJXnI=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-helper-replace-supers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz", + "integrity": "sha1-v22/5Dk40XNpohPKiov3S2qQqxo=", + "dev": true, + "requires": { + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-helpers": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz", + "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-messages": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", + "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-check-es2015-constants": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz", + "integrity": "sha1-NRV7EBQm/S/9PaP3XH0ekYNbv4o=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-syntax-async-functions": { + "version": "6.13.0", + "resolved": "http://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", + "integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=", + "dev": true + }, + "babel-plugin-transform-async-to-bluebird": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-bluebird/-/babel-plugin-transform-async-to-bluebird-1.1.1.tgz", + "integrity": "sha1-Ruo+fFr2KXgqyfHtG3zTj4Qlr9Q=", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.8.0", + "babel-plugin-syntax-async-functions": "^6.8.0", + "babel-template": "^6.9.0", + "babel-traverse": "^6.10.4" + } + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz", + "integrity": "sha1-RSaSy3EdX3ncf4XkQM5BufJE0iE=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz", + "integrity": "sha1-u8UbSflk1wy42OC5ToICRs46YUE=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz", + "integrity": "sha1-1w9SmcEwjQXBL0Y4E7CgnnOxiV8=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "lodash": "^4.17.4" + } + }, + "babel-plugin-transform-es2015-classes": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz", + "integrity": "sha1-WkxYpQyclGHlZLSyo7+ryXolhNs=", + "dev": true, + "requires": { + "babel-helper-define-map": "^6.24.1", + "babel-helper-function-name": "^6.24.1", + "babel-helper-optimise-call-expression": "^6.24.1", + "babel-helper-replace-supers": "^6.24.1", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz", + "integrity": "sha1-b+Ko0WiV1WNPTNmZttNICjCBWbM=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz", + "integrity": "sha1-mXux8auWf2gtKwh2/jWNYOdlxW0=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz", + "integrity": "sha1-c+s9MQypaePvnskcU3QabxV2Qj4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-for-of": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz", + "integrity": "sha1-9HyVsrYT3x0+zC/bdXNiPHUkhpE=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-function-name": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz", + "integrity": "sha1-g0yJhTvDaxrw86TF26qU/Y6sqos=", + "dev": true, + "requires": { + "babel-helper-function-name": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz", + "integrity": "sha1-T1SgLWzWbPkVKAAZox0xklN3yi4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-modules-amd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz", + "integrity": "sha1-Oz5UAXI5hC1tGcMBHEvS8AoA0VQ=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.26.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz", + "integrity": "sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q==", + "dev": true, + "requires": { + "babel-plugin-transform-strict-mode": "^6.24.1", + "babel-runtime": "^6.26.0", + "babel-template": "^6.26.0", + "babel-types": "^6.26.0" + } + }, + "babel-plugin-transform-es2015-modules-systemjs": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz", + "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=", + "dev": true, + "requires": { + "babel-helper-hoist-variables": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-modules-umd": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz", + "integrity": "sha1-rJl+YoXNGO1hdq22B9YCNErThGg=", + "dev": true, + "requires": { + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-object-super": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz", + "integrity": "sha1-JM72muIcuDp/hgPa0CH1cusnj40=", + "dev": true, + "requires": { + "babel-helper-replace-supers": "^6.24.1", + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-parameters": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz", + "integrity": "sha1-V6w1GrScrxSpfNE7CfZv3wpiXys=", + "dev": true, + "requires": { + "babel-helper-call-delegate": "^6.24.1", + "babel-helper-get-function-arity": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-template": "^6.24.1", + "babel-traverse": "^6.24.1", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz", + "integrity": "sha1-JPh11nIch2YbvZmkYi5R8U3jiqA=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-spread": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz", + "integrity": "sha1-1taKmfia7cRTbIGlQujdnxdG+NE=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz", + "integrity": "sha1-AMHNsaynERLN8M9hJsLta0V8zbw=", + "dev": true, + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz", + "integrity": "sha1-qEs0UPfp+PH2g51taH2oS7EjbY0=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz", + "integrity": "sha1-3sCfHN3/lLUqxz1QXITfWdzOs3I=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz", + "integrity": "sha1-04sS9C6nMj9yk4fxinxa4frrNek=", + "dev": true, + "requires": { + "babel-helper-regex": "^6.24.1", + "babel-runtime": "^6.22.0", + "regexpu-core": "^2.0.0" + } + }, + "babel-plugin-transform-regenerator": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz", + "integrity": "sha1-4HA2lvveJ/Cj78rPi03KL3s6jy8=", + "dev": true, + "requires": { + "regenerator-transform": "^0.10.0" + } + }, + "babel-plugin-transform-runtime": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz", + "integrity": "sha1-iEkNRGUC6puOfvsP4J7E2ZR5se4=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0" + } + }, + "babel-plugin-transform-strict-mode": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz", + "integrity": "sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g=", + "dev": true, + "requires": { + "babel-runtime": "^6.22.0", + "babel-types": "^6.24.1" + } + }, + "babel-polyfill": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", + "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "regenerator-runtime": "^0.10.5" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", + "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", + "dev": true + } + } + }, + "babel-preset-es2015": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz", + "integrity": "sha1-1EBQ1rwsn+6nAqrzjXJ6AhBTiTk=", + "dev": true, + "requires": { + "babel-plugin-check-es2015-constants": "^6.22.0", + "babel-plugin-transform-es2015-arrow-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoped-functions": "^6.22.0", + "babel-plugin-transform-es2015-block-scoping": "^6.24.1", + "babel-plugin-transform-es2015-classes": "^6.24.1", + "babel-plugin-transform-es2015-computed-properties": "^6.24.1", + "babel-plugin-transform-es2015-destructuring": "^6.22.0", + "babel-plugin-transform-es2015-duplicate-keys": "^6.24.1", + "babel-plugin-transform-es2015-for-of": "^6.22.0", + "babel-plugin-transform-es2015-function-name": "^6.24.1", + "babel-plugin-transform-es2015-literals": "^6.22.0", + "babel-plugin-transform-es2015-modules-amd": "^6.24.1", + "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", + "babel-plugin-transform-es2015-modules-systemjs": "^6.24.1", + "babel-plugin-transform-es2015-modules-umd": "^6.24.1", + "babel-plugin-transform-es2015-object-super": "^6.24.1", + "babel-plugin-transform-es2015-parameters": "^6.24.1", + "babel-plugin-transform-es2015-shorthand-properties": "^6.24.1", + "babel-plugin-transform-es2015-spread": "^6.22.0", + "babel-plugin-transform-es2015-sticky-regex": "^6.24.1", + "babel-plugin-transform-es2015-template-literals": "^6.22.0", + "babel-plugin-transform-es2015-typeof-symbol": "^6.22.0", + "babel-plugin-transform-es2015-unicode-regex": "^6.24.1", + "babel-plugin-transform-regenerator": "^6.24.1" + } + }, + "babel-register": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz", + "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=", + "dev": true, + "requires": { + "babel-core": "^6.26.0", + "babel-runtime": "^6.26.0", + "core-js": "^2.5.0", + "home-or-tmp": "^2.0.0", + "lodash": "^4.17.4", + "mkdirp": "^0.5.1", + "source-map-support": "^0.4.15" + } + }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "babel-template": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz", + "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "babel-traverse": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "lodash": "^4.17.4" + } + }, + "babel-traverse": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", + "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", + "dev": true, + "requires": { + "babel-code-frame": "^6.26.0", + "babel-messages": "^6.23.0", + "babel-runtime": "^6.26.0", + "babel-types": "^6.26.0", + "babylon": "^6.18.0", + "debug": "^2.6.8", + "globals": "^9.18.0", + "invariant": "^2.2.2", + "lodash": "^4.17.4" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "esutils": "^2.0.2", + "lodash": "^4.17.4", + "to-fast-properties": "^1.0.3" + } + }, + "babylon": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", + "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "base-x": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.4.tgz", + "integrity": "sha512-UYOadoSIkEI/VrRGSG6qp93rp2WdokiAiNYDfGW5qURAY8GiAQkvMbwNNSDYiVJopqv4gCna7xqf4rrNGp+5AA==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "binary-extensions": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.12.0.tgz", + "integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==", + "dev": true + }, + "bluebird": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", + "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==" + }, + "bn.js": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true, + "requires": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "browser-pack": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz", + "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "combine-source-map": "~0.8.0", + "defined": "^1.0.0", + "safe-buffer": "^5.1.1", + "through2": "^2.0.0", + "umd": "^3.0.0" + } + }, + "browser-request": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz", + "integrity": "sha1-ns5bWsqJopkyJC4Yv5M975h2zBc=" + }, + "browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "dev": true, + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "browserify": { + "version": "14.5.0", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-14.5.0.tgz", + "integrity": "sha512-gKfOsNQv/toWz+60nSPfYzuwSEdzvV2WdxrVPUbPD/qui44rAkB3t3muNtmmGYHqrG56FGwX9SUEQmzNLAeS7g==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "assert": "^1.4.0", + "browser-pack": "^6.0.1", + "browser-resolve": "^1.11.0", + "browserify-zlib": "~0.2.0", + "buffer": "^5.0.2", + "cached-path-relative": "^1.0.0", + "concat-stream": "~1.5.1", + "console-browserify": "^1.1.0", + "constants-browserify": "~1.0.0", + "crypto-browserify": "^3.0.0", + "defined": "^1.0.0", + "deps-sort": "^2.0.0", + "domain-browser": "~1.1.0", + "duplexer2": "~0.1.2", + "events": "~1.1.0", + "glob": "^7.1.0", + "has": "^1.0.0", + "htmlescape": "^1.1.0", + "https-browserify": "^1.0.0", + "inherits": "~2.0.1", + "insert-module-globals": "^7.0.0", + "labeled-stream-splicer": "^2.0.0", + "module-deps": "^4.0.8", + "os-browserify": "~0.3.0", + "parents": "^1.0.1", + "path-browserify": "~0.0.0", + "process": "~0.11.0", + "punycode": "^1.3.2", + "querystring-es3": "~0.2.0", + "read-only-stream": "^2.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.1.4", + "shasum": "^1.0.0", + "shell-quote": "^1.6.1", + "stream-browserify": "^2.0.0", + "stream-http": "^2.0.0", + "string_decoder": "~1.0.0", + "subarg": "^1.0.0", + "syntax-error": "^1.1.1", + "through2": "^2.0.0", + "timers-browserify": "^1.0.1", + "tty-browserify": "~0.0.0", + "url": "~0.11.0", + "util": "~0.10.1", + "vm-browserify": "~0.0.1", + "xtend": "^4.0.0" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.0.1", + "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" + } + }, + "browserify-shim": { + "version": "3.8.14", + "resolved": "https://registry.npmjs.org/browserify-shim/-/browserify-shim-3.8.14.tgz", + "integrity": "sha1-vxBXAmky0yU8de991xTzuHft7Gs=", + "dev": true, + "requires": { + "exposify": "~0.5.0", + "mothership": "~0.2.0", + "rename-function-calls": "~0.1.0", + "resolve": "~0.6.1", + "through": "~2.3.4" + }, + "dependencies": { + "resolve": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-0.6.3.tgz", + "integrity": "sha1-3ZV5gufnNt699TtYpN2RdUV13UY=", + "dev": true + } + } + }, + "browserify-sign": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", + "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", + "dev": true, + "requires": { + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", + "requires": { + "base-x": "^3.0.2" + } + }, + "buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", + "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "cached-path-relative": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz", + "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==", + "dev": true + }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "^0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "catharsis": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.9.tgz", + "integrity": "sha1-mMyJDKZS3S7w5ws3klMQ/56Q/Is=", + "dev": true, + "requires": { + "underscore-contrib": "~0.3.0" + } + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "chokidar": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", + "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "dev": true, + "requires": { + "anymatch": "^1.3.0", + "async-each": "^1.0.0", + "fsevents": "^1.0.0", + "glob-parent": "^2.0.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^2.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0" + } + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "combine-source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", + "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", + "dev": true, + "requires": { + "convert-source-map": "~1.1.0", + "inline-source-map": "~0.6.0", + "lodash.memoize": "~3.0.3", + "source-map": "~0.5.3" + }, + "dependencies": { + "convert-source-map": { + "version": "1.1.3", + "resolved": "http://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=", + "dev": true + } + } + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "dev": true + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", + "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "~2.0.0", + "typedarray": "~0.0.5" + }, + "dependencies": { + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", + "dev": true + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "convert-source-map": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", + "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-js": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", + "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "create-ecdh": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", + "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "d": { + "version": "1.0.0", + "resolved": "http://registry.npmjs.org/d/-/d-1.0.0.tgz", + "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "dev": true, + "requires": { + "es5-ext": "^0.10.9" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + }, + "dependencies": { + "object-keys": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", + "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", + "dev": true + } + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", + "dev": true + }, + "del": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", + "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", + "dev": true, + "requires": { + "globby": "^6.1.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "p-map": "^1.1.1", + "pify": "^3.0.0", + "rimraf": "^2.2.8" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "deps-sort": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.0.tgz", + "integrity": "sha1-CRckkC6EZYJg65EHSMzNGvbiH7U=", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "shasum": "^1.0.0", + "subarg": "^1.0.0", + "through2": "^2.0.0" + } + }, + "des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "detect-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", + "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "detective": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-4.7.1.tgz", + "integrity": "sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig==", + "dev": true, + "requires": { + "acorn": "^5.2.1", + "defined": "^1.0.0" + }, + "dependencies": { + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + } + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "domain-browser": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", + "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=", + "dev": true + }, + "dot-parts": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dot-parts/-/dot-parts-1.0.1.tgz", + "integrity": "sha1-iEvXvPwwgv+tL+XbU+SU2PPgdD8=", + "dev": true + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "dev": true, + "requires": { + "readable-stream": "^2.0.2" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "elliptic": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz", + "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "es-abstract": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", + "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", + "dev": true, + "requires": { + "es-to-primitive": "^1.1.1", + "function-bind": "^1.1.1", + "has": "^1.0.1", + "is-callable": "^1.1.3", + "is-regex": "^1.0.4" + } + }, + "es-to-primitive": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", + "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "es5-ext": { + "version": "0.10.46", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.46.tgz", + "integrity": "sha512-24XxRvJXNFwEMpJb3nOkiRJKRoupmjYmOPVlI65Qy2SrtxwOTB+g6ODjBKOtwEHbYrhWRty9xxOWLNdClT2djw==", + "dev": true, + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.1", + "next-tick": "1" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-symbol": "3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "es6-weak-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", + "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.14", + "es6-iterator": "^2.0.1", + "es6-symbol": "^3.1.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.1.0.tgz", + "integrity": "sha1-xmOSP24gqtSNDA+knzHG1PSTYM8=", + "dev": true, + "requires": { + "esprima": "~1.0.4", + "estraverse": "~1.5.0", + "esutils": "~1.0.0", + "source-map": "~0.1.30" + }, + "dependencies": { + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", + "dev": true + }, + "esutils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz", + "integrity": "sha1-gVHTWOIMisx/t0XnRywAJf5JZXA=", + "dev": true + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", + "dev": true, + "requires": { + "es6-map": "^0.1.3", + "es6-weak-map": "^2.0.1", + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, + "eslint": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", + "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", + "dev": true, + "requires": { + "babel-code-frame": "^6.16.0", + "chalk": "^1.1.3", + "concat-stream": "^1.5.2", + "debug": "^2.1.1", + "doctrine": "^2.0.0", + "escope": "^3.6.0", + "espree": "^3.4.0", + "esquery": "^1.0.0", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "file-entry-cache": "^2.0.0", + "glob": "^7.0.3", + "globals": "^9.14.0", + "ignore": "^3.2.0", + "imurmurhash": "^0.1.4", + "inquirer": "^0.12.0", + "is-my-json-valid": "^2.10.0", + "is-resolvable": "^1.0.0", + "js-yaml": "^3.5.1", + "json-stable-stringify": "^1.0.0", + "levn": "^0.3.0", + "lodash": "^4.0.0", + "mkdirp": "^0.5.0", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "path-is-inside": "^1.0.1", + "pluralize": "^1.2.1", + "progress": "^1.1.8", + "require-uncached": "^1.0.2", + "shelljs": "^0.7.5", + "strip-bom": "^3.0.0", + "strip-json-comments": "~2.0.1", + "table": "^3.7.8", + "text-table": "~0.2.0", + "user-home": "^2.0.0" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "user-home": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", + "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0" + } + } + } + }, + "eslint-config-google": { + "version": "0.7.1", + "resolved": "http://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.7.1.tgz", + "integrity": "sha1-VZj4SY6eB4Qg80uASVuNlZ9lH7I=", + "dev": true + }, + "espree": { + "version": "3.5.4", + "resolved": "http://registry.npmjs.org/espree/-/espree-3.5.4.tgz", + "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==", + "dev": true, + "requires": { + "acorn": "^5.5.0", + "acorn-jsx": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + } + } + }, + "esprima-fb": { + "version": "3001.1.0-dev-harmony-fb", + "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz", + "integrity": "sha1-t303q8046gt3Qmu4vCkizmtCZBE=", + "dev": true + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, + "estraverse": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz", + "integrity": "sha1-hno+jlip+EYYr7bC3bzZFrfLr3E=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "events": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "exorcist": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/exorcist/-/exorcist-0.4.0.tgz", + "integrity": "sha1-EjD/3t2SSPQvvM+LSkTUyrKePGQ=", + "dev": true, + "requires": { + "minimist": "0.0.5", + "mold-source-map": "~0.4.0", + "nave": "~0.5.1" + }, + "dependencies": { + "minimist": { + "version": "0.0.5", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.5.tgz", + "integrity": "sha1-16oye87PUY+RBqxrjwA/o7zqhWY=", + "dev": true + } + } + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true, + "requires": { + "is-posix-bracket": "^0.1.0" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true, + "requires": { + "fill-range": "^2.1.0" + } + }, + "expect": { + "version": "1.20.2", + "resolved": "http://registry.npmjs.org/expect/-/expect-1.20.2.tgz", + "integrity": "sha1-1Fj+TFYAQDa64yMkFqP2Nh8E+WU=", + "dev": true, + "requires": { + "define-properties": "~1.1.2", + "has": "^1.0.1", + "is-equal": "^1.5.1", + "is-regex": "^1.0.3", + "object-inspect": "^1.1.0", + "object-keys": "^1.0.9", + "tmatch": "^2.0.1" + }, + "dependencies": { + "object-keys": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", + "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", + "dev": true + } + } + }, + "exposify": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/exposify/-/exposify-0.5.0.tgz", + "integrity": "sha1-+S0AlMJls/VT4fpFagOhiD0QWcw=", + "dev": true, + "requires": { + "globo": "~1.1.0", + "map-obj": "~1.0.1", + "replace-requires": "~1.0.3", + "through2": "~0.4.0", + "transformify": "~0.1.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "0.4.2", + "resolved": "http://registry.npmjs.org/through2/-/through2-0.4.2.tgz", + "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s=", + "dev": true, + "requires": { + "readable-stream": "~1.0.17", + "xtend": "~2.1.1" + } + }, + "xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", + "dev": true, + "requires": { + "object-keys": "~0.4.0" + } + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "^1.2.1", + "object-assign": "^4.0.1" + } + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "dev": true, + "requires": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + } + }, + "find-parent-dir": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.0.tgz", + "integrity": "sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ=", + "dev": true + }, + "flat-cache": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.2.tgz", + "integrity": "sha512-KByBY8c98sLUAGpnmjEdWTrtrLZRtZdwds+kAL/ciFXTCb7AZgqKsAnVnYFQj1hxepwO8JKN/8AsRWwLq+RK0A==", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "del": "^3.0.0", + "graceful-fs": "^4.1.2", + "write": "^0.2.1" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fs-readdir-recursive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", + "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", + "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", + "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", + "dev": true, + "optional": true, + "requires": { + "safer-buffer": "^2.1.0" + } + }, + "ignore-walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "minipass": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", + "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.1", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", + "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.0.tgz", + "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==", + "dev": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz", + "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.1.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.3.tgz", + "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.10.tgz", + "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.7.tgz", + "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.5.1", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "optional": true, + "requires": { + "glob": "^7.0.5" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.1.tgz", + "integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==", + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.0.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.2.4", + "minizlib": "^1.1.0", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.1", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", + "dev": true + } + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dev": true, + "requires": { + "is-property": "^1.0.2" + } + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "^1.0.0" + } + }, + "get-assigned-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", + "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "requires": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true, + "requires": { + "is-glob": "^2.0.0" + } + }, + "globals": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", + "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "dev": true + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, + "globo": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/globo/-/globo-1.1.0.tgz", + "integrity": "sha1-DSYJiVXepCLrIAGxBImLChAcqvM=", + "dev": true, + "requires": { + "accessory": "~1.1.0", + "is-defined": "~1.0.0", + "ternary": "~1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "handlebars": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.12.tgz", + "integrity": "sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA==", + "dev": true, + "requires": { + "async": "^2.5.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + }, + "dependencies": { + "async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", + "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "dev": true, + "requires": { + "lodash": "^4.17.10" + } + }, + "commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", + "dev": true, + "optional": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "uglify-js": { + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz", + "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==", + "dev": true, + "optional": true, + "requires": { + "commander": "~2.17.1", + "source-map": "~0.6.1" + } + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "has-require": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/has-require/-/has-require-1.2.2.tgz", + "integrity": "sha1-khZ1qxMNvZdo/I2o8ajiQt+kF3Q=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.3" + } + }, + "has-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", + "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", + "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "hash.js": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz", + "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "home-or-tmp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz", + "integrity": "sha1-42w/LSyufXRqhX440Y1fMqeILbg=", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.1" + } + }, + "htmlescape": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", + "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=", + "dev": true + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "ieee754": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", + "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==", + "dev": true + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "inline-source-map": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", + "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", + "dev": true, + "requires": { + "source-map": "~0.5.3" + } + }, + "inquirer": { + "version": "0.12.0", + "resolved": "http://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", + "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", + "dev": true, + "requires": { + "ansi-escapes": "^1.1.0", + "ansi-regex": "^2.0.0", + "chalk": "^1.0.0", + "cli-cursor": "^1.0.1", + "cli-width": "^2.0.0", + "figures": "^1.3.5", + "lodash": "^4.3.0", + "readline2": "^1.0.1", + "run-async": "^0.1.0", + "rx-lite": "^3.1.2", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.0", + "through": "^2.3.6" + } + }, + "insert-module-globals": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.0.tgz", + "integrity": "sha512-VE6NlW+WGn2/AeOMd496AHFYmE7eLKkUY6Ty31k4og5vmA3Fjuwe9v6ifH6Xx/Hz27QvdoMoviw1/pqWRB09Sw==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "acorn-node": "^1.5.2", + "combine-source-map": "^0.8.0", + "concat-stream": "^1.6.1", + "is-buffer": "^1.1.0", + "path-is-absolute": "^1.0.1", + "process": "~0.11.0", + "through2": "^2.0.0", + "undeclared-identifiers": "^1.1.2", + "xtend": "^4.0.0" + }, + "dependencies": { + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + } + } + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dev": true, + "requires": { + "loose-envify": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-arrow-function": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-arrow-function/-/is-arrow-function-2.0.3.tgz", + "integrity": "sha1-Kb4sLY2UUIUri7r7Y1unuNjofsI=", + "dev": true, + "requires": { + "is-callable": "^1.0.4" + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-boolean-object": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.0.tgz", + "integrity": "sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-callable": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", + "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==", + "dev": true + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-date-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", + "dev": true + }, + "is-defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-defined/-/is-defined-1.0.0.tgz", + "integrity": "sha1-HwfKZ9Vx9ZTEsUQVpF9774j5K/U=", + "dev": true + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/is-equal/-/is-equal-1.5.5.tgz", + "integrity": "sha1-XoXxlX4FKIMkf+s4aWWju6Ffuz0=", + "dev": true, + "requires": { + "has": "^1.0.1", + "is-arrow-function": "^2.0.3", + "is-boolean-object": "^1.0.0", + "is-callable": "^1.1.3", + "is-date-object": "^1.0.1", + "is-generator-function": "^1.0.6", + "is-number-object": "^1.0.3", + "is-regex": "^1.0.3", + "is-string": "^1.0.4", + "is-symbol": "^1.0.1", + "object.entries": "^1.0.4" + } + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true, + "requires": { + "is-primitive": "^2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true, + "requires": { + "is-extglob": "^1.0.0" + } + }, + "is-my-ip-valid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", + "dev": true + }, + "is-my-json-valid": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.19.0.tgz", + "integrity": "sha512-mG0f/unGX1HZ5ep4uhRaPOS8EkAY8/j6mDRMJrutq4CqhoJWYp7qAlonIPy3TV7p3ju4TK9fo/PbnoksWmsp5Q==", + "dev": true, + "requires": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^4.0.0", + "xtend": "^4.0.0" + } + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "is-number-object": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.3.tgz", + "integrity": "sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=", + "dev": true + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "dev": true, + "requires": { + "is-path-inside": "^1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-regex": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", + "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "dev": true, + "requires": { + "has": "^1.0.1" + } + }, + "is-resolvable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-string": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.4.tgz", + "integrity": "sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ=", + "dev": true + }, + "is-symbol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", + "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", + "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + } + } + }, + "js2xmlparser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-3.0.0.tgz", + "integrity": "sha1-P7YOqgicVED5MZ9RdgzNB+JJlzM=", + "dev": true, + "requires": { + "xmlcreate": "^1.0.1" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "jsdoc": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.5.5.tgz", + "integrity": "sha512-6PxB65TAU4WO0Wzyr/4/YhlGovXl0EVYfpKbpSroSj0qBxT4/xod/l40Opkm38dRHRdQgdeY836M0uVnJQG7kg==", + "dev": true, + "requires": { + "babylon": "7.0.0-beta.19", + "bluebird": "~3.5.0", + "catharsis": "~0.8.9", + "escape-string-regexp": "~1.0.5", + "js2xmlparser": "~3.0.0", + "klaw": "~2.0.0", + "marked": "~0.3.6", + "mkdirp": "~0.5.1", + "requizzle": "~0.2.1", + "strip-json-comments": "~2.0.1", + "taffydb": "2.6.2", + "underscore": "~1.8.3" + }, + "dependencies": { + "babylon": { + "version": "7.0.0-beta.19", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.19.tgz", + "integrity": "sha512-Vg0C9s/REX6/WIXN37UKpv5ZhRi6A4pjHlpkE34+8/a6c2W1Q692n3hmc+SZG5lKRnaExLUbxtJ1SVT+KaCQ/A==", + "dev": true + } + } + }, + "jsesc": { + "version": "1.3.0", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stable-stringify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", + "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json5": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", + "dev": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "klaw": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-2.0.0.tgz", + "integrity": "sha1-WcEo4Nxc5BAgEVEZTuucv4WGUPY=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.9" + } + }, + "labeled-stream-splicer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.1.tgz", + "integrity": "sha512-MC94mHZRvJ3LfykJlTUipBqenZz1pacOZEMhhQ8dMGcDHs0SBE5GbsavUXV7YtP3icBW17W0Zy1I0lfASmo9Pg==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "isarray": "^2.0.4", + "stream-splicer": "^2.0.0" + }, + "dependencies": { + "isarray": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.4.tgz", + "integrity": "sha512-GMxXOiUirWg1xTKRipM0Ek07rX+ubx4nNVElTJdNLYmNO/2YrDkgJGw9CljXn+r4EWiDQg/8lsRdHyg2PJuUaA==", + "dev": true + } + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "dev": true + }, + "lodash.memoize": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", + "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=", + "dev": true + }, + "loglevel": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz", + "integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po=" + }, + "lolex": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", + "integrity": "sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY=", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "marked": { + "version": "0.3.19", + "resolved": "http://registry.npmjs.org/marked/-/marked-0.3.19.tgz", + "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==", + "dev": true + }, + "math-random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", + "dev": true + }, + "matrix-mock-request": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/matrix-mock-request/-/matrix-mock-request-1.2.2.tgz", + "integrity": "sha512-9u86m6rOsKekNkqUUkStWXNULrY9G+9ibwolfrmgqTmgR76EGCr9fovM+JPWn4U+TbrewvHMALpPw8OxRg0ExA==", + "dev": true, + "requires": { + "bluebird": "^3.5.0", + "expect": "^1.20.2" + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "requires": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } + }, + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" + }, + "mime-types": { + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "requires": { + "mime-db": "~1.37.0" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "mocha-jenkins-reporter": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mocha-jenkins-reporter/-/mocha-jenkins-reporter-0.4.1.tgz", + "integrity": "sha512-IqnIylrkKJG0lxeoawRkhv/uiYojMEw3o9TQOpDFarPYKVq4ymngVPwsyfMB0XMDqtDbOTOCviFg8xOLHb80/Q==", + "dev": true, + "requires": { + "diff": "1.0.7", + "mkdirp": "0.5.1", + "mocha": "^5.2.0", + "xml": "^1.0.1" + }, + "dependencies": { + "diff": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.0.7.tgz", + "integrity": "sha1-JLuwAcSn1VIhaefKvbLCgU7ZHPQ=", + "dev": true + } + } + }, + "module-deps": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-4.1.1.tgz", + "integrity": "sha1-IyFYM/HaE/1gbMuAh7RIUty4If0=", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "browser-resolve": "^1.7.0", + "cached-path-relative": "^1.0.0", + "concat-stream": "~1.5.0", + "defined": "^1.0.0", + "detective": "^4.0.0", + "duplexer2": "^0.1.2", + "inherits": "^2.0.1", + "parents": "^1.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.1.3", + "stream-combiner2": "^1.1.1", + "subarg": "^1.0.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "mold-source-map": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/mold-source-map/-/mold-source-map-0.4.0.tgz", + "integrity": "sha1-z2fgsxxHq5uttcnCVlGGISe7gxc=", + "dev": true, + "requires": { + "convert-source-map": "^1.1.0", + "through": "~2.2.7" + }, + "dependencies": { + "through": { + "version": "2.2.7", + "resolved": "http://registry.npmjs.org/through/-/through-2.2.7.tgz", + "integrity": "sha1-bo4hIAGR1OtqmfbwEN9Gqhxusr0=", + "dev": true + } + } + }, + "mothership": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/mothership/-/mothership-0.2.0.tgz", + "integrity": "sha1-k9SKL7w+UOKl/I7VhvW8RMZfmpk=", + "dev": true, + "requires": { + "find-parent-dir": "~0.3.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "mute-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", + "dev": true + }, + "nan": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", + "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==", + "dev": true, + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nave": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/nave/-/nave-0.5.3.tgz", + "integrity": "sha1-Ws7HI3WFblx2yDvSGmjXE+tfG6Q=", + "dev": true + }, + "next-tick": { + "version": "1.0.0", + "resolved": "http://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", + "dev": true + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "object-inspect": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", + "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==", + "dev": true + }, + "object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", + "dev": true + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "object.entries": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz", + "integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.6.1", + "function-bind": "^1.1.0", + "has": "^1.0.1" + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true, + "requires": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "outpipe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/outpipe/-/outpipe-1.1.1.tgz", + "integrity": "sha1-UM+GFjZeh+Ax4ppeyTOaPaRyX6I=", + "dev": true, + "requires": { + "shell-quote": "^1.4.2" + } + }, + "output-file-sync": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-1.1.2.tgz", + "integrity": "sha1-0KM+7+YaIF+suQCS6CZZjVJFznY=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.4", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.0" + } + }, + "p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "dev": true + }, + "pako": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz", + "integrity": "sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg==", + "dev": true + }, + "parents": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", + "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", + "dev": true, + "requires": { + "path-platform": "~0.11.15" + } + }, + "parse-asn1": { + "version": "5.1.1", + "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", + "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", + "dev": true, + "requires": { + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "requires": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + } + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "patch-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/patch-text/-/patch-text-1.0.2.tgz", + "integrity": "sha1-S/NuZeUXM9bpjwz2LgkDTaoDSKw=", + "dev": true + }, + "path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "path-platform": { + "version": "0.11.15", + "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", + "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=", + "dev": true + }, + "pbkdf2": { + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", + "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "dev": true, + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pluralize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", + "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", + "dev": true + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "progress": { + "version": "1.1.8", + "resolved": "http://registry.npmjs.org/progress/-/progress-1.1.8.tgz", + "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", + "dev": true + }, + "psl": { + "version": "1.1.29", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "randomatic": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", + "dev": true, + "requires": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "dependencies": { + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "randombytes": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", + "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "read-only-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", + "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", + "dev": true, + "requires": { + "readable-stream": "^2.0.2" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + } + } + }, + "readline2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "mute-stream": "0.0.5" + } + }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, + "regenerate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", + "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, + "regenerator-transform": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.10.1.tgz", + "integrity": "sha512-PJepbvDbuK1xgIgnau7Y90cwaAmO/LCLMI2mPvaXq2heGMR3aWW5/BQvYrhJ8jgmQjXewXvBjzfqKcVOmhjZ6Q==", + "dev": true, + "requires": { + "babel-runtime": "^6.18.0", + "babel-types": "^6.19.0", + "private": "^0.1.6" + } + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "dev": true, + "requires": { + "is-equal-shallow": "^0.1.3" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "regexpu-core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz", + "integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=", + "dev": true, + "requires": { + "regenerate": "^1.2.1", + "regjsgen": "^0.2.0", + "regjsparser": "^0.1.4" + } + }, + "regjsgen": { + "version": "0.2.0", + "resolved": "http://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", + "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", + "dev": true + }, + "regjsparser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", + "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "rename-function-calls": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/rename-function-calls/-/rename-function-calls-0.1.1.tgz", + "integrity": "sha1-f4M2nAB6MAf2q+MDPM+BaGoQjgE=", + "dev": true, + "requires": { + "detective": "~3.1.0" + }, + "dependencies": { + "detective": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-3.1.0.tgz", + "integrity": "sha1-d3gkRKt1K4jKG+Lp0KA5Xx2iXu0=", + "dev": true, + "requires": { + "escodegen": "~1.1.0", + "esprima-fb": "3001.1.0-dev-harmony-fb" + } + } + } + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "replace-requires": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/replace-requires/-/replace-requires-1.0.4.tgz", + "integrity": "sha1-AUtzMLa54lV7cQQ7ZvsCZgw79mc=", + "dev": true, + "requires": { + "detective": "^4.5.0", + "has-require": "~1.2.1", + "patch-text": "~1.0.2", + "xtend": "~4.0.0" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "require-uncached": { + "version": "1.0.3", + "resolved": "http://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "^0.1.0", + "resolve-from": "^1.0.0" + } + }, + "requizzle": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.1.tgz", + "integrity": "sha1-aUPDUwxNmn5G8c3dUcFY/GcM294=", + "dev": true, + "requires": { + "underscore": "~1.6.0" + }, + "dependencies": { + "underscore": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", + "dev": true + } + } + }, + "resolve": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", + "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "dev": true, + "requires": { + "path-parse": "^1.0.5" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "requires": { + "align-text": "^0.1.1" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "^7.0.5" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "run-async": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", + "dev": true, + "requires": { + "once": "^1.3.0" + } + }, + "rx-lite": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", + "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", + "dev": true + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "sha.js": { + "version": "2.4.11", + "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shasum": { + "version": "1.0.2", + "resolved": "http://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz", + "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=", + "dev": true, + "requires": { + "json-stable-stringify": "~0.0.0", + "sha.js": "~2.4.4" + } + }, + "shell-quote": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", + "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", + "dev": true, + "requires": { + "array-filter": "~0.0.0", + "array-map": "~0.0.0", + "array-reduce": "~0.0.0", + "jsonify": "~0.0.0" + } + }, + "shelljs": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", + "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", + "dev": true, + "requires": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + } + }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=", + "dev": true + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", + "dev": true + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "http://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dev": true, + "requires": { + "source-map": "^0.5.6" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "sourceify": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/sourceify/-/sourceify-0.1.0.tgz", + "integrity": "sha1-C1b+V/lFc1DZJliBCq/afA9EA0w=", + "dev": true, + "requires": { + "convert-source-map": "^1.1.3", + "through2": "^2.0.0" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", + "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "stream-browserify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", + "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "stream-combiner2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "dev": true, + "requires": { + "duplexer2": "~0.1.0", + "readable-stream": "^2.0.2" + } + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "stream-splicer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.0.tgz", + "integrity": "sha1-G2O+Q4oTPktnHMGTUZdgAXWRDYM=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.2" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "dev": true, + "requires": { + "minimist": "^1.1.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "syntax-error": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", + "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", + "dev": true, + "requires": { + "acorn-node": "^1.2.0" + } + }, + "table": { + "version": "3.8.3", + "resolved": "http://registry.npmjs.org/table/-/table-3.8.3.tgz", + "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", + "dev": true, + "requires": { + "ajv": "^4.7.0", + "ajv-keywords": "^1.0.0", + "chalk": "^1.1.1", + "lodash": "^4.0.0", + "slice-ansi": "0.0.4", + "string-width": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "requires": { + "co": "^4.6.0", + "json-stable-stringify": "^1.0.1" + } + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "requires": { + "jsonify": "~0.0.0" + } + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "taffydb": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", + "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=", + "dev": true + }, + "ternary": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ternary/-/ternary-1.0.0.tgz", + "integrity": "sha1-RXAnJWCMlJnUapYQ6bDkn/JveJ4=", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "timers-browserify": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", + "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "dev": true, + "requires": { + "process": "~0.11.0" + } + }, + "tmatch": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-2.0.1.tgz", + "integrity": "sha1-DFYkbzPzDaG409colauvFmYPOM8=", + "dev": true + }, + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-fast-properties": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + } + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "transformify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/transformify/-/transformify-0.1.2.tgz", + "integrity": "sha1-mk9CoVRDPdcnuAV1Qoo8nlSJ6/E=", + "dev": true, + "requires": { + "readable-stream": "~1.1.9" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", + "dev": true + }, + "tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "umd": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", + "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==", + "dev": true + }, + "undeclared-identifiers": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.2.tgz", + "integrity": "sha512-13EaeocO4edF/3JKime9rD7oB6QI8llAGhgn5fKOPyfkJbRb6NFv9pYV6dFEmpa4uRjKeBqLZP8GpuzqHlKDMQ==", + "dev": true, + "requires": { + "acorn-node": "^1.3.0", + "get-assigned-identifiers": "^1.2.0", + "simple-concat": "^1.0.0", + "xtend": "^4.0.1" + } + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", + "dev": true + }, + "underscore-contrib": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/underscore-contrib/-/underscore-contrib-0.3.0.tgz", + "integrity": "sha1-ZltmwkeD+PorGMn4y7Dix9SMJsc=", + "dev": true, + "requires": { + "underscore": "1.6.0" + }, + "dependencies": { + "underscore": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", + "dev": true + } + } + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + } + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "user-home": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", + "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", + "dev": true + }, + "util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "v8flags": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz", + "integrity": "sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ=", + "dev": true, + "requires": { + "user-home": "^1.1.1" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "vm-browserify": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", + "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", + "dev": true, + "requires": { + "indexof": "0.0.1" + } + }, + "watchify": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/watchify/-/watchify-3.11.0.tgz", + "integrity": "sha512-7jWG0c3cKKm2hKScnSAMUEUjRJKXUShwMPk0ASVhICycQhwND3IMAdhJYmc1mxxKzBUJTSF5HZizfrKrS6BzkA==", + "dev": true, + "requires": { + "anymatch": "^1.3.0", + "browserify": "^16.1.0", + "chokidar": "^1.0.0", + "defined": "^1.0.0", + "outpipe": "^1.1.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + }, + "dependencies": { + "browserify": { + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-16.2.3.tgz", + "integrity": "sha512-zQt/Gd1+W+IY+h/xX2NYMW4orQWhqSwyV+xsblycTtpOuB27h1fZhhNQuipJ4t79ohw4P4mMem0jp/ZkISQtjQ==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "assert": "^1.4.0", + "browser-pack": "^6.0.1", + "browser-resolve": "^1.11.0", + "browserify-zlib": "~0.2.0", + "buffer": "^5.0.2", + "cached-path-relative": "^1.0.0", + "concat-stream": "^1.6.0", + "console-browserify": "^1.1.0", + "constants-browserify": "~1.0.0", + "crypto-browserify": "^3.0.0", + "defined": "^1.0.0", + "deps-sort": "^2.0.0", + "domain-browser": "^1.2.0", + "duplexer2": "~0.1.2", + "events": "^2.0.0", + "glob": "^7.1.0", + "has": "^1.0.0", + "htmlescape": "^1.1.0", + "https-browserify": "^1.0.0", + "inherits": "~2.0.1", + "insert-module-globals": "^7.0.0", + "labeled-stream-splicer": "^2.0.0", + "mkdirp": "^0.5.0", + "module-deps": "^6.0.0", + "os-browserify": "~0.3.0", + "parents": "^1.0.1", + "path-browserify": "~0.0.0", + "process": "~0.11.0", + "punycode": "^1.3.2", + "querystring-es3": "~0.2.0", + "read-only-stream": "^2.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.1.4", + "shasum": "^1.0.0", + "shell-quote": "^1.6.1", + "stream-browserify": "^2.0.0", + "stream-http": "^2.0.0", + "string_decoder": "^1.1.1", + "subarg": "^1.0.0", + "syntax-error": "^1.1.1", + "through2": "^2.0.0", + "timers-browserify": "^1.0.1", + "tty-browserify": "0.0.1", + "url": "~0.11.0", + "util": "~0.10.1", + "vm-browserify": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "detective": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.1.0.tgz", + "integrity": "sha512-TFHMqfOvxlgrfVzTEkNBSh9SvSNX/HfF4OFI2QFGCyPm02EsyILqnUeb5P6q7JZ3SFNTBL5t2sePRgrN4epUWQ==", + "dev": true, + "requires": { + "acorn-node": "^1.3.0", + "defined": "^1.0.0", + "minimist": "^1.1.1" + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "dev": true + }, + "events": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", + "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==", + "dev": true + }, + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "module-deps": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.0.tgz", + "integrity": "sha512-hKPmO06so6bL/ZvqVNVqdTVO8UAYsi3tQWlCa+z9KuWhoN4KDQtb5hcqQQv58qYiDE21wIvnttZEPiDgEbpwbA==", + "dev": true, + "requires": { + "JSONStream": "^1.0.3", + "browser-resolve": "^1.7.0", + "cached-path-relative": "^1.0.0", + "concat-stream": "~1.6.0", + "defined": "^1.0.0", + "detective": "^5.0.2", + "duplexer2": "^0.1.2", + "inherits": "^2.0.1", + "parents": "^1.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.4.0", + "stream-combiner2": "^1.1.1", + "subarg": "^1.0.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "vm-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", + "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", + "dev": true + } + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, + "xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", + "dev": true + }, + "xmlcreate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz", + "integrity": "sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } +} From a30845f9ce55c4fac4fb46a6f6c4d2979d0dd824 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Nov 2018 08:03:23 +0000 Subject: [PATCH 420/472] lint --- src/crypto/OlmDevice.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index c87ef9525..0ac53b3ed 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -583,10 +583,12 @@ OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKe // Use the session that has most recently received a message let idxOfMin = 0; for (let i = 1; i < sessionInfos.length; i++) { + const thisSessInfo = sessionInfos[i]; + const minSessInfo = sessionInfos[idxOfMin]; if ( - sessionInfos[i].lastReceivedMessageTs < sessionInfos[idxOfMin].lastReceiveMessageTs || ( - sessionInfos[i].lastReceivedMessageTs === sessionInfos[idxOfMin].lastReceiveMessageTs && - sessionInfos[i].sessionId < sessinInfos[idxOfMin].sessionId + thisSessInfo.lastReceivedMessageTs < minSessInfo.lastReceiveMessageTs || ( + thisSessInfo.lastReceivedMessageTs === minSessInfo.lastReceiveMessageTs && + thisSessInfo.sessionId < minSessInfo.sessionId ) ) { idxOfMin = i; From dbb6d8ac71524303a9a6f09bb24bbca0323b523b Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 4 Nov 2018 19:43:18 +0000 Subject: [PATCH 421/472] Set access_token and user_id after login in with username and password. --- src/base-apis.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/base-apis.js b/src/base-apis.js index 6dc9169dd..d00ab64dc 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -277,7 +277,21 @@ MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback) return this.login("m.login.password", { user: user, password: password, - }, callback); + }, (error, response) => { + if (response && response.access_token) { + this._http.opts.accessToken = response.access_token; + } + + if (response && response.user_id) { + this.credentials = { + userId: response.user_id, + }; + } + + if(callback) { + callback(error, response); + } + }); }; /** From b0d0782a729357aad63079a044c8c1ea48fa82fc Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 16:02:30 +0000 Subject: [PATCH 422/472] Linting. --- src/base-apis.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base-apis.js b/src/base-apis.js index d00ab64dc..8836c59d8 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -288,7 +288,7 @@ MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback) }; } - if(callback) { + if (callback) { callback(error, response); } }); From 11be68ad4919d416891236308c8d8c072effc35c Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 16:40:11 +0000 Subject: [PATCH 423/472] Refactor code to base 'login' method. --- src/base-apis.js | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/base-apis.js b/src/base-apis.js index 8836c59d8..ad0f84b45 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -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, ); }; @@ -277,21 +289,7 @@ MatrixBaseApis.prototype.loginWithPassword = function(user, password, callback) return this.login("m.login.password", { user: user, password: password, - }, (error, response) => { - if (response && response.access_token) { - this._http.opts.accessToken = response.access_token; - } - - if (response && response.user_id) { - this.credentials = { - userId: response.user_id, - }; - } - - if (callback) { - callback(error, response); - } - }); + }, callback); }; /** From d40d7e18f5b4f1d54f67e8a9b0721420037a8db5 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 17:06:39 +0000 Subject: [PATCH 424/472] Update CHANGELOG. --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4295d74fe..8c5b7ec7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +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. + 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) From 977d5331c0ca8779692fd14a1ceaef9cbe463370 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 17:17:22 +0000 Subject: [PATCH 425/472] Update CHANGELOG. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c5b7ec7d..666558997 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ Changes in [0.13.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0. BREAKING CHANGE ---------------- - * `MatrixClient::login` now sets client `access_token` and `user_id` following successful login. + * `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) ================================================================================================== From fbe174fb64e2c04b9dbd6654f9f95258b454db29 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 14 Nov 2018 10:53:42 +0100 Subject: [PATCH 426/472] v0.13.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8c9c9544f..0c872e9fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.12.1", + "version": "0.13.0", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From ebf20d5b2cb03e23d13bfc8243cb32a287270372 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Nov 2018 14:20:55 +0000 Subject: [PATCH 427/472] Don't force more than one new session per device per hour --- src/crypto/algorithms/megolm.js | 2 +- src/crypto/index.js | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index b4a78334a..ae018a42c 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -499,7 +499,7 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function( }, }); logger.debug( - `Re-shared key for session ${sessionId} with {userId}:{device.deviceId}`, + `Re-shared key for session ${sessionId} with ${userId}:${device.deviceId}`, ); }; diff --git a/src/crypto/index.js b/src/crypto/index.js index 7c0ac003a..26292f3bb 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -41,6 +41,8 @@ export function isCryptoAvailable() { return Boolean(global.Olm); } +const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000; + /** * Cryptography bits * @@ -120,6 +122,15 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId, // has happened for a given room. This is delayed // to avoid loading room members as long as possible. this._roomDeviceTrackingState = {}; + + // The timestamp of the last time we forced establishment + // of a new session for each device, in milliseconds. + // { + // userId: { + // deviceId: 1234567890000, + // }, + // } + this._lastNewSessionForced = {}; } utils.inherits(Crypto, EventEmitter); @@ -1180,6 +1191,19 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) { return; } + // check when we last forced a new session with this device: if we've already done so + // recently, don't do it again. + this._lastNewSessionForced[sender] = this._lastNewSessionForced[sender] || {}; + const lastNewSessionForced = this._lastNewSessionForced[sender][deviceKey] || 0; + if (lastNewSessionForced + MIN_FORCE_SESSION_INTERVAL_MS > Date.now()) { + logger.debug( + "New session already forced with device " + sender + ":" + deviceKey + + " at " + lastNewSessionForced + ": not forcing another", + ); + return; + } + this._lastNewSessionForced[sender][deviceKey] = Date.now(); + // establish a new olm session with this device since we're failing to decrypt messages // on a current session. // Note that an undecryptable message from another device could easily be spoofed - From 5bc68c0c6dad66d868b33f78643189f166d85db3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Nov 2018 14:29:03 +0000 Subject: [PATCH 428/472] Handle last received message ts being undefined --- src/crypto/OlmDevice.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 0ac53b3ed..c63f0e3a5 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -584,10 +584,15 @@ OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKe let idxOfMin = 0; for (let i = 1; i < sessionInfos.length; i++) { const thisSessInfo = sessionInfos[i]; + const thisLastReceived = thisSessInfo.lastReceivedMessageTs === undefined ? + 0 : thisSessInfo.lastReceivedMessageTs; + const minSessInfo = sessionInfos[idxOfMin]; + const minLastReceived = minSessInfo.lastReceivedMessageTs === undefined ? + 0 : thisSessInfo.lastReceivedMessageTs; if ( - thisSessInfo.lastReceivedMessageTs < minSessInfo.lastReceiveMessageTs || ( - thisSessInfo.lastReceivedMessageTs === minSessInfo.lastReceiveMessageTs && + thisLastReceived < minLastReceived || ( + thisLastReceived === minLastReceived && thisSessInfo.sessionId < minSessInfo.sessionId ) ) { From 408407b33d082cb5c600d71b8e33e3e94e3d7260 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Nov 2018 14:34:36 +0000 Subject: [PATCH 429/472] Fix typo --- src/crypto/OlmDevice.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index c63f0e3a5..61d61789d 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -589,7 +589,7 @@ OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKe const minSessInfo = sessionInfos[idxOfMin]; const minLastReceived = minSessInfo.lastReceivedMessageTs === undefined ? - 0 : thisSessInfo.lastReceivedMessageTs; + 0 : minSessInfo.lastReceivedMessageTs; if ( thisLastReceived < minLastReceived || ( thisLastReceived === minLastReceived && From 3c85bd55d31384ce0173fe47f0feea32fe43e6f7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Nov 2018 14:57:48 +0000 Subject: [PATCH 430/472] Time goes forwards --- src/crypto/OlmDevice.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/crypto/OlmDevice.js b/src/crypto/OlmDevice.js index 61d61789d..e5b05fa4b 100644 --- a/src/crypto/OlmDevice.js +++ b/src/crypto/OlmDevice.js @@ -581,25 +581,25 @@ OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKe return null; } // Use the session that has most recently received a message - let idxOfMin = 0; + let idxOfBest = 0; for (let i = 1; i < sessionInfos.length; i++) { const thisSessInfo = sessionInfos[i]; const thisLastReceived = thisSessInfo.lastReceivedMessageTs === undefined ? 0 : thisSessInfo.lastReceivedMessageTs; - const minSessInfo = sessionInfos[idxOfMin]; - const minLastReceived = minSessInfo.lastReceivedMessageTs === undefined ? - 0 : minSessInfo.lastReceivedMessageTs; + const bestSessInfo = sessionInfos[idxOfBest]; + const bestLastReceived = bestSessInfo.lastReceivedMessageTs === undefined ? + 0 : bestSessInfo.lastReceivedMessageTs; if ( - thisLastReceived < minLastReceived || ( - thisLastReceived === minLastReceived && - thisSessInfo.sessionId < minSessInfo.sessionId + thisLastReceived > bestLastReceived || ( + thisLastReceived === bestLastReceived && + thisSessInfo.sessionId < bestSessInfo.sessionId ) ) { - idxOfMin = i; + idxOfBest = i; } } - return sessionInfos[idxOfMin].sessionId; + return sessionInfos[idxOfBest].sessionId; }; /** From 23efd0850d50a3108b2586a6b3ec937a17a1b7b8 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Sun, 4 Nov 2018 21:49:17 +0000 Subject: [PATCH 431/472] Add function to get currently joined rooms. --- src/base-apis.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/base-apis.js b/src/base-apis.js index ad0f84b45..b0b132fed 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -939,6 +939,14 @@ 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); +}; // Room Directory operations // ========================= From 988be628043f931b932ef25ca9f4fd38ee9a54d3 Mon Sep 17 00:00:00 2001 From: Richard Lewis Date: Mon, 5 Nov 2018 00:08:04 +0000 Subject: [PATCH 432/472] Add function to get currently joined room members. --- src/base-apis.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/base-apis.js b/src/base-apis.js index b0b132fed..1515ca735 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -948,6 +948,20 @@ MatrixBaseApis.prototype.getJoinedRooms = function() { 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 // ========================= From 980d55a2f3caf3e5906c76811459d0e8d3f524a3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 14 Nov 2018 18:18:11 +0100 Subject: [PATCH 433/472] Prepare changelog for v0.13.1 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 666558997..46c90e029 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +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) From 6ce7b30b72926aa4c165934282bf0e2cc1fd18c9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 14 Nov 2018 18:18:12 +0100 Subject: [PATCH 434/472] v0.13.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0c872e9fd..9aadecd3f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.13.0", + "version": "0.13.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From c9917e4079c5f846b794465ba54254fee586b4a4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 15 Nov 2018 10:03:16 +0000 Subject: [PATCH 435/472] lint --- src/crypto/algorithms/megolm.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index ae018a42c..0bbf1ca52 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -439,7 +439,10 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function( } const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId]; if (sentChainIndex === undefined) { - logger.debug("Session ID " + sessionId + " never shared with device " + userId + ":" + device.deviceId); + logger.debug( + "Session ID " + sessionId + " never shared with device " + + userId + ":" + device.deviceId, + ); return; } From fc9d6a6d472341005a77e5d873546c67c4e10c75 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 15 Nov 2018 15:06:39 +0000 Subject: [PATCH 436/472] Prepare changelog for v0.14.0-rc.1 --- CHANGELOG.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46c90e029..39c37ea2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,47 @@ +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) From 3222b11346cd10cdf28ac2ea7d8394a590c9c19c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 15 Nov 2018 15:06:39 +0000 Subject: [PATCH 437/472] v0.14.0-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 95f1b1389..7fbfd59bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.13.1", + "version": "0.14.0-rc.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From c53c6a94d72ed936c8af7fe46af0fd465bd602d1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 15 Nov 2018 16:38:35 +0000 Subject: [PATCH 438/472] Update package-lock so versions are consistent --- package-lock.json | 372 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 320 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index b56c65f2f..df95672aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,202 @@ { "name": "matrix-js-sdk", - "version": "0.12.0", + "version": "0.13.1", "lockfileVersion": 1, "requires": true, "dependencies": { + "@babel/code-frame": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz", + "integrity": "sha512-cuAuTTIQ9RqcFRJ/Y8PvTh+paepNcaGxwQwjIDRWPXmzzyAeCO4KqS9ikMvq0MCbRk6GlYKwfzStrcP3/jSL8g==", + "dev": true, + "requires": { + "@babel/highlight": "7.0.0-beta.44" + } + }, + "@babel/generator": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.44.tgz", + "integrity": "sha512-5xVb7hlhjGcdkKpMXgicAVgx8syK5VJz193k0i/0sLP6DzE6lRrU1K3B/rFefgdo9LPGMAOOOAWW4jycj07ShQ==", + "dev": true, + "requires": { + "@babel/types": "7.0.0-beta.44", + "jsesc": "^2.5.1", + "lodash": "^4.2.0", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + }, + "dependencies": { + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz", + "integrity": "sha512-MHRG2qZMKMFaBavX0LWpfZ2e+hLloT++N7rfM3DYOMUOGCD8cVjqZpwiL8a0bOX3IYcQev1ruciT0gdFFRTxzg==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "7.0.0-beta.44", + "@babel/template": "7.0.0-beta.44", + "@babel/types": "7.0.0-beta.44" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz", + "integrity": "sha512-w0YjWVwrM2HwP6/H3sEgrSQdkCaxppqFeJtAnB23pRiJB5E/O9Yp7JAAeWBl+gGEgmBFinnTyOv2RN7rcSmMiw==", + "dev": true, + "requires": { + "@babel/types": "7.0.0-beta.44" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz", + "integrity": "sha512-aQ7QowtkgKKzPGf0j6u77kBMdUFVBKNHw2p/3HX/POt5/oz8ec5cs0GwlgM8Hz7ui5EwJnzyfRmkNF1Nx1N7aA==", + "dev": true, + "requires": { + "@babel/types": "7.0.0-beta.44" + } + }, + "@babel/highlight": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.44.tgz", + "integrity": "sha512-Il19yJvy7vMFm8AVAh6OZzaFoAd0hbkeMZiX3P5HGD+z7dyI7RzndHB0dg6Urh/VAFfHtpOIzDUSxmY6coyZWQ==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/template": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.44.tgz", + "integrity": "sha512-w750Sloq0UNifLx1rUqwfbnC6uSUk0mfwwgGRfdLiaUzfAOiH0tHJE6ILQIUi3KYkjiCDTskoIsnfqZvWLBDng==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.44", + "@babel/types": "7.0.0-beta.44", + "babylon": "7.0.0-beta.44", + "lodash": "^4.2.0" + }, + "dependencies": { + "babylon": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.44.tgz", + "integrity": "sha512-5Hlm13BJVAioCHpImtFqNOF2H3ieTOHd0fmFGMxOJ9jgeFqeAwsv3u5P5cR7CSeFrkgHsT19DgFJkHV0/Mcd8g==", + "dev": true + } + } + }, + "@babel/traverse": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.44.tgz", + "integrity": "sha512-UHuDz8ukQkJCDASKHf+oDt3FVUzFd+QYfuBIsiNu/4+/ix6pP/C+uQZJ6K1oEfbCMv/IKWbgDEh7fcsnIE5AtA==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.44", + "@babel/generator": "7.0.0-beta.44", + "@babel/helper-function-name": "7.0.0-beta.44", + "@babel/helper-split-export-declaration": "7.0.0-beta.44", + "@babel/types": "7.0.0-beta.44", + "babylon": "7.0.0-beta.44", + "debug": "^3.1.0", + "globals": "^11.1.0", + "invariant": "^2.2.0", + "lodash": "^4.2.0" + }, + "dependencies": { + "babylon": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.44.tgz", + "integrity": "sha512-5Hlm13BJVAioCHpImtFqNOF2H3ieTOHd0fmFGMxOJ9jgeFqeAwsv3u5P5cR7CSeFrkgHsT19DgFJkHV0/Mcd8g==", + "dev": true + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "globals": { + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.9.0.tgz", + "integrity": "sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==", + "dev": true + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.44.tgz", + "integrity": "sha512-5eTV4WRmqbaFM3v9gHAIljEQJU4Ssc6fxL61JN+Oe2ga/BwyjzjamwkCVVAQjHGuAX8i0BWo42dshL8eO5KfLQ==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.2.0", + "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + } + } + }, "JSONStream": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", @@ -380,15 +573,25 @@ } }, "babel-eslint": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-7.2.3.tgz", - "integrity": "sha1-sv4tgBJkcPXBlELcdXJTqJdxCCc=", + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-8.2.6.tgz", + "integrity": "sha512-aCdHjhzcILdP8c9lej7hvXKvQieyRt20SF102SIGyY4cUIiw6UaAtK4j2o3dXX74jEmy0TJ0CEhv4fTIM3SzcA==", "dev": true, "requires": { - "babel-code-frame": "^6.22.0", - "babel-traverse": "^6.23.1", - "babel-types": "^6.23.0", - "babylon": "^6.17.0" + "@babel/code-frame": "7.0.0-beta.44", + "@babel/traverse": "7.0.0-beta.44", + "@babel/types": "7.0.0-beta.44", + "babylon": "7.0.0-beta.44", + "eslint-scope": "3.7.1", + "eslint-visitor-keys": "^1.0.0" + }, + "dependencies": { + "babylon": { + "version": "7.0.0-beta.44", + "resolved": "http://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.44.tgz", + "integrity": "sha512-5Hlm13BJVAioCHpImtFqNOF2H3ieTOHd0fmFGMxOJ9jgeFqeAwsv3u5P5cR7CSeFrkgHsT19DgFJkHV0/Mcd8g==", + "dev": true + } } }, "babel-generator": { @@ -1112,9 +1315,9 @@ "dev": true }, "browserify": { - "version": "14.5.0", - "resolved": "https://registry.npmjs.org/browserify/-/browserify-14.5.0.tgz", - "integrity": "sha512-gKfOsNQv/toWz+60nSPfYzuwSEdzvV2WdxrVPUbPD/qui44rAkB3t3muNtmmGYHqrG56FGwX9SUEQmzNLAeS7g==", + "version": "16.2.3", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-16.2.3.tgz", + "integrity": "sha512-zQt/Gd1+W+IY+h/xX2NYMW4orQWhqSwyV+xsblycTtpOuB27h1fZhhNQuipJ4t79ohw4P4mMem0jp/ZkISQtjQ==", "dev": true, "requires": { "JSONStream": "^1.0.3", @@ -1124,15 +1327,15 @@ "browserify-zlib": "~0.2.0", "buffer": "^5.0.2", "cached-path-relative": "^1.0.0", - "concat-stream": "~1.5.1", + "concat-stream": "^1.6.0", "console-browserify": "^1.1.0", "constants-browserify": "~1.0.0", "crypto-browserify": "^3.0.0", "defined": "^1.0.0", "deps-sort": "^2.0.0", - "domain-browser": "~1.1.0", + "domain-browser": "^1.2.0", "duplexer2": "~0.1.2", - "events": "~1.1.0", + "events": "^2.0.0", "glob": "^7.1.0", "has": "^1.0.0", "htmlescape": "^1.1.0", @@ -1140,7 +1343,8 @@ "inherits": "~2.0.1", "insert-module-globals": "^7.0.0", "labeled-stream-splicer": "^2.0.0", - "module-deps": "^4.0.8", + "mkdirp": "^0.5.0", + "module-deps": "^6.0.0", "os-browserify": "~0.3.0", "parents": "^1.0.1", "path-browserify": "~0.0.0", @@ -1154,32 +1358,35 @@ "shell-quote": "^1.6.1", "stream-browserify": "^2.0.0", "stream-http": "^2.0.0", - "string_decoder": "~1.0.0", + "string_decoder": "^1.1.1", "subarg": "^1.0.0", "syntax-error": "^1.1.1", "through2": "^2.0.0", "timers-browserify": "^1.0.1", - "tty-browserify": "~0.0.0", + "tty-browserify": "0.0.1", "url": "~0.11.0", "util": "~0.10.1", - "vm-browserify": "~0.0.1", + "vm-browserify": "^1.0.0", "xtend": "^4.0.0" }, "dependencies": { + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", "dev": true - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } } } }, @@ -1518,6 +1725,21 @@ "object-visit": "^1.0.0" } }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, "combine-source-map": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", @@ -1925,9 +2147,9 @@ } }, "domain-browser": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", - "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", "dev": true }, "dot-parts": { @@ -2201,6 +2423,30 @@ "integrity": "sha1-VZj4SY6eB4Qg80uASVuNlZ9lH7I=", "dev": true }, + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "dependencies": { + "estraverse": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", + "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "dev": true + }, "espree": { "version": "3.5.4", "resolved": "http://registry.npmjs.org/espree/-/espree-3.5.4.tgz", @@ -2282,9 +2528,9 @@ } }, "events": { - "version": "1.1.1", - "resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-2.1.0.tgz", + "integrity": "sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==", "dev": true }, "evp_bytestokey": { @@ -3554,12 +3800,6 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -4527,26 +4767,57 @@ } }, "module-deps": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-4.1.1.tgz", - "integrity": "sha1-IyFYM/HaE/1gbMuAh7RIUty4If0=", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.0.tgz", + "integrity": "sha512-hKPmO06so6bL/ZvqVNVqdTVO8UAYsi3tQWlCa+z9KuWhoN4KDQtb5hcqQQv58qYiDE21wIvnttZEPiDgEbpwbA==", "dev": true, "requires": { "JSONStream": "^1.0.3", "browser-resolve": "^1.7.0", "cached-path-relative": "^1.0.0", - "concat-stream": "~1.5.0", + "concat-stream": "~1.6.0", "defined": "^1.0.0", - "detective": "^4.0.0", + "detective": "^5.0.2", "duplexer2": "^0.1.2", "inherits": "^2.0.1", "parents": "^1.0.0", "readable-stream": "^2.0.2", - "resolve": "^1.1.3", + "resolve": "^1.4.0", "stream-combiner2": "^1.1.1", "subarg": "^1.0.0", "through2": "^2.0.0", "xtend": "^4.0.0" + }, + "dependencies": { + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "detective": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.1.0.tgz", + "integrity": "sha512-TFHMqfOvxlgrfVzTEkNBSh9SvSNX/HfF4OFI2QFGCyPm02EsyILqnUeb5P6q7JZ3SFNTBL5t2sePRgrN4epUWQ==", + "dev": true, + "requires": { + "acorn-node": "^1.3.0", + "defined": "^1.0.0", + "minimist": "^1.1.1" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } } }, "mold-source-map": { @@ -6629,13 +6900,10 @@ } }, "vm-browserify": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz", - "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=", - "dev": true, - "requires": { - "indexof": "0.0.1" - } + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz", + "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==", + "dev": true }, "watchify": { "version": "3.11.0", From 578cb4e268f7881cb101df51462d132d0f931212 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 15 Nov 2018 19:00:37 +0000 Subject: [PATCH 439/472] Add 'getSsoLoginUrl' function --- src/base-apis.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/base-apis.js b/src/base-apis.js index 1515ca735..f05c06d7c 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -310,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 = "cas"; + } + return this._http.getUrl("/login/"+loginType+"/redirect", { "redirectUrl": redirectUrl, - }, httpApi.PREFIX_UNSTABLE); + }, httpApi.PREFIX_R0); }; /** From 80f562643f32c5a314886d9e2d50ee009651d94c Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 16 Nov 2018 10:38:44 +0100 Subject: [PATCH 440/472] never return null as a name --- src/models/room.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/room.js b/src/models/room.js index c58b4a632..1235a6859 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -1610,7 +1610,7 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) { function memberNamesToRoomName(names, count = (names.length + 1)) { const countWithoutMe = count - 1; if (!names.length) { - return count <= 1 ? "Empty room" : null; + return "Empty room"; } else if (names.length === 1 && countWithoutMe <= 1) { return names[0]; } else if (names.length === 2 && countWithoutMe <= 2) { From 2602c155d0b7bd8174e8b994cf6af9e9845c9614 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 16 Nov 2018 11:31:08 +0000 Subject: [PATCH 441/472] Handle crypto db version upgrades --- src/crypto/store/indexeddb-crypto-store.js | 22 +++++++++++++------ src/errors.js | 25 ++++++++++++++++++++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index 3a7d6d1d6..ec83e2aa5 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -21,6 +21,7 @@ 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. @@ -107,16 +108,25 @@ export default class IndexedDBCryptoStore { }, ); }).catch((e) => { + 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) => { - logger.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; diff --git a/src/errors.js b/src/errors.js index 409fafd9c..f3d88d643 100644 --- a/src/errors.js +++ b/src/errors.js @@ -1,6 +1,6 @@ // can't just do InvalidStoreError extends Error // because of http://babeljs.io/docs/usage/caveats/#classes -function InvalidStoreError(reason, value) { +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]); @@ -22,4 +22,25 @@ InvalidStoreError.prototype = Object.create(Error.prototype, { }); Reflect.setPrototypeOf(InvalidStoreError, Error); -module.exports.InvalidStoreError = InvalidStoreError; + +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); From d99a22d68d687c02ca50c175960fee17b56abf4f Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 16 Nov 2018 14:46:18 +0000 Subject: [PATCH 442/472] Update to new API Also fix test & remove debug logging from test --- spec/unit/crypto.spec.js | 3 +-- src/crypto/index.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js index 47d7d2d67..e3cabbac6 100644 --- a/spec/unit/crypto.spec.js +++ b/spec/unit/crypto.spec.js @@ -69,6 +69,7 @@ describe("Crypto", function() { mockBaseApis = { sendToDevice: expect.createSpy(), + getKeyBackupVersion: expect.createSpy(), }; mockRoomList = {}; @@ -111,9 +112,7 @@ describe("Crypto", function() { getSender: expect.createSpy().andReturn('@bob:home.server'), }); - console.log("waiting"); await prom; - console.log("done"); }); }); }); diff --git a/src/crypto/index.js b/src/crypto/index.js index 89c152f5b..bcc5105e7 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1459,7 +1459,7 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) { // on a current session. // Note that an undecryptable message from another device could easily be spoofed - // is there anything we can do to mitigate this? - const device = this._deviceList.getDeviceByIdentityKey(sender, algorithm, deviceKey); + const device = this._deviceList.getDeviceByIdentityKey(algorithm, deviceKey); const devicesByUser = {}; devicesByUser[sender] = [device]; await olmlib.ensureOlmSessionsForDevices( From 5b51096e37da0b64838a10a4072cd0a603f8c8ab Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Mon, 19 Nov 2018 13:08:41 +0000 Subject: [PATCH 443/472] fix default login type for getSsoLoginUrl --- src/base-apis.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base-apis.js b/src/base-apis.js index f05c06d7c..8fd1bd0f1 100644 --- a/src/base-apis.js +++ b/src/base-apis.js @@ -322,7 +322,7 @@ MatrixBaseApis.prototype.getCasLoginUrl = function(redirectUrl) { */ MatrixBaseApis.prototype.getSsoLoginUrl = function(redirectUrl, loginType) { if (loginType === undefined) { - loginType = "cas"; + loginType = "sso"; } return this._http.getUrl("/login/"+loginType+"/redirect", { "redirectUrl": redirectUrl, From 2306caa62fcbcf137ecf0305c41aa03fa024be6b Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 19 Nov 2018 15:09:42 +0000 Subject: [PATCH 444/472] Prepare changelog for v0.14.0 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39c37ea2c..e833882e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +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) From ae85c209ab0d58c900bc3db1802cffc1be880b85 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 19 Nov 2018 15:09:43 +0000 Subject: [PATCH 445/472] v0.14.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7fbfd59bf..c686ad16c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.14.0-rc.1", + "version": "0.14.0", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From 44d99277fe600c23272ec27c8ef4513ce1d977de Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 20 Nov 2018 13:09:59 +0000 Subject: [PATCH 446/472] Support passphrase-based e2e key backups --- src/client.js | 50 +++++++++++++++++---- src/crypto/backup_password.js | 81 +++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 src/crypto/backup_password.js diff --git a/src/client.js b/src/client.js index 9ff1762c0..da7d5218a 100644 --- a/src/client.js +++ b/src/client.js @@ -49,6 +49,7 @@ import RoomList from './crypto/RoomList'; import Crypto from './crypto'; import { isCryptoAvailable } from './crypto'; import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey'; +import { keyForNewBackup, keyForExistingBackup } from './crypto/backup_password'; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -860,22 +861,37 @@ MatrixClient.prototype.disableKeyBackup = function() { * Set up the data required to create a new backup version. The backup version * will not be created and enabled until createKeyBackupVersion is called. * + * @param {string} password Passphrase string that can be entered by the user + * when restoring the backup as an alternative to entering the recovery key. + * Optional. + * * @returns {object} Object that can be passed to createKeyBackupVersion and * additionally has a 'recovery_key' member with the user-facing recovery key string. */ -MatrixClient.prototype.prepareKeyBackupVersion = function() { +MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); } const decryption = new global.Olm.PkDecryption(); try { - const publicKey = decryption.generate_key(); + let privateKey; + let publicKey; + let authData = {}; + if (password) { + const keyInfo = await keyForNewBackup(password); + publicKey = decryption.init_with_private_key(keyInfo.key); + authData.private_key_salt = keyInfo.salt; + authData.private_key_iterations = keyInfo.iterations; + } else { + publicKey = decryption.generate_key(); + } + + authData.public_key = publicKey; + return { algorithm: olmlib.MEGOLM_BACKUP_ALGORITHM, - auth_data: { - public_key: publicKey, - }, + auth_data: authData, recovery_key: encodeRecoveryKey(decryption.get_private_key()), }; } finally { @@ -992,8 +1008,28 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) { } }; -MatrixClient.prototype.restoreKeyBackups = function( +MatrixClient.prototype.restoreKeyBackupWithPassword = async function( + password, targetRoomId, targetSessionId, version, +) { + const backupInfo = await this.getKeyBackupVersion(); + + const privKey = keyForExistingBackup(backupInfo, password); + return this._restoreKeyBackup( + privKey, targetRoomId, targetSessionId, version, + ); +}; + +MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function( recoveryKey, targetRoomId, targetSessionId, version, +) { + const privKey = decodeRecoveryKey(recoveryKey); + return this._restoreKeyBackup( + privKey, targetRoomId, targetSessionId, version, + ); +}; + +MatrixClient.prototype._restoreKeyBackup = function( + privKey, targetRoomId, targetSessionId, version, ) { if (this._crypto === null) { throw new Error("End-to-end encryption disabled"); @@ -1003,8 +1039,6 @@ MatrixClient.prototype.restoreKeyBackups = function( const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version); - // FIXME: see the FIXME in createKeyBackupVersion - const privkey = decodeRecoveryKey(recoveryKey); const decryption = new global.Olm.PkDecryption(); try { decryption.init_with_private_key(privkey); diff --git a/src/crypto/backup_password.js b/src/crypto/backup_password.js new file mode 100644 index 000000000..946b99681 --- /dev/null +++ b/src/crypto/backup_password.js @@ -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.private_key_salt, + backupData.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); +} From cb51799246579fa45c3290b7659e417c71b98d4d Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 20 Nov 2018 16:15:29 +0000 Subject: [PATCH 447/472] Make backup restore work --- src/client.js | 4 ++-- src/crypto/backup_password.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client.js b/src/client.js index da7d5218a..cd236aedb 100644 --- a/src/client.js +++ b/src/client.js @@ -1013,7 +1013,7 @@ MatrixClient.prototype.restoreKeyBackupWithPassword = async function( ) { const backupInfo = await this.getKeyBackupVersion(); - const privKey = keyForExistingBackup(backupInfo, password); + const privKey = await keyForExistingBackup(backupInfo, password); return this._restoreKeyBackup( privKey, targetRoomId, targetSessionId, version, ); @@ -1041,7 +1041,7 @@ MatrixClient.prototype._restoreKeyBackup = function( const decryption = new global.Olm.PkDecryption(); try { - decryption.init_with_private_key(privkey); + decryption.init_with_private_key(privKey); } catch(e) { decryption.free(); throw e; diff --git a/src/crypto/backup_password.js b/src/crypto/backup_password.js index 946b99681..1a6d1f284 100644 --- a/src/crypto/backup_password.js +++ b/src/crypto/backup_password.js @@ -33,8 +33,8 @@ export async function keyForExistingBackup(backupData, password) { } return await deriveKey( - password, backupData.private_key_salt, - backupData.private_key_iterations, + password, backupData.auth_data.private_key_salt, + backupData.auth_data.private_key_iterations, ); } From 6047838f53092aa44b3ea3f10a7a7dfdfd353079 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 20 Nov 2018 16:17:58 +0000 Subject: [PATCH 448/472] lint --- src/client.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client.js b/src/client.js index cd236aedb..8ac8b6a1f 100644 --- a/src/client.js +++ b/src/client.js @@ -875,9 +875,8 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { const decryption = new global.Olm.PkDecryption(); try { - let privateKey; let publicKey; - let authData = {}; + const authData = {}; if (password) { const keyInfo = await keyForNewBackup(password); publicKey = decryption.init_with_private_key(keyInfo.key); From eeea70640e6b5c5e769f393e8f50535726e0fa83 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 20 Nov 2018 16:28:29 +0000 Subject: [PATCH 449/472] Add randomString factored out from client secret --- src/client.js | 10 ++-------- src/randomstring.js | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 src/randomstring.js diff --git a/src/client.js b/src/client.js index 8ac8b6a1f..daaaf52c9 100644 --- a/src/client.js +++ b/src/client.js @@ -50,6 +50,7 @@ import Crypto from './crypto'; import { isCryptoAvailable } from './crypto'; import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey'; import { keyForNewBackup, keyForExistingBackup } from './crypto/backup_password'; +import { randomString } from './randomstring'; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -3862,14 +3863,7 @@ MatrixClient.prototype.getEventMapper = function() { * @return {string} A new client secret */ MatrixClient.prototype.generateClientSecret = function() { - let ret = ""; - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - for (let i = 0; i < 32; i++) { - ret += chars.charAt(Math.floor(Math.random() * chars.length)); - } - - return ret; + return randomString(32); }; /** */ diff --git a/src/randomstring.js b/src/randomstring.js new file mode 100644 index 000000000..7ebe4ed78 --- /dev/null +++ b/src/randomstring.js @@ -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; +} From abd2ac71688074b3d333c38e590c28c027a1cdb4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 20 Nov 2018 16:34:04 +0000 Subject: [PATCH 450/472] Rename backup API call in test --- spec/unit/crypto/backup.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index bf9fe8edd..a654312b7 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -435,7 +435,7 @@ describe("MegolmBackup", function() { client._http.authedRequest = function() { return Promise.resolve(KEY_BACKUP_DATA); }; - return client.restoreKeyBackups( + return client.restoreKeyBackupWithRecoveryKey( "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", ROOM_ID, SESSION_ID, @@ -458,7 +458,7 @@ describe("MegolmBackup", function() { }, }); }; - return client.restoreKeyBackups( + return client.restoreKeyBackupWithRecoveryKey( "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", ).then(() => { return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); From 092f4217b02f31c17f437895dac48af62a4f2c65 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 21 Nov 2018 17:56:02 +0000 Subject: [PATCH 451/472] docs --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index daaaf52c9..1840b3cac 100644 --- a/src/client.js +++ b/src/client.js @@ -866,7 +866,7 @@ MatrixClient.prototype.disableKeyBackup = function() { * when restoring the backup as an alternative to entering the recovery key. * Optional. * - * @returns {object} Object that can be passed to createKeyBackupVersion and + * @returns {Promise} Object that can be passed to createKeyBackupVersion and * additionally has a 'recovery_key' member with the user-facing recovery key string. */ MatrixClient.prototype.prepareKeyBackupVersion = async function(password) { From 712490b671578659eb7bfdb2c8db7c4ceb984f1f Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 21 Nov 2018 18:43:13 +0000 Subject: [PATCH 452/472] Use a vaguely recent node --- jenkins.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins.sh b/jenkins.sh index 047497bb5..87a1137b3 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -5,7 +5,7 @@ set -x export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" -nvm use 6 || exit $? +nvm use 10 || exit $? npm install || exit $? RC=0 From f70746c50f7c90ffcdd55a2e1e9da14662f1d155 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 16 Nov 2018 11:31:08 +0000 Subject: [PATCH 453/472] Handle crypto db version upgrades --- src/crypto/store/indexeddb-crypto-store.js | 22 +++++++++++++------ src/errors.js | 25 ++++++++++++++++++++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/crypto/store/indexeddb-crypto-store.js b/src/crypto/store/indexeddb-crypto-store.js index c9210da23..b5a189cd5 100644 --- a/src/crypto/store/indexeddb-crypto-store.js +++ b/src/crypto/store/indexeddb-crypto-store.js @@ -21,6 +21,7 @@ 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. @@ -107,16 +108,25 @@ export default class IndexedDBCryptoStore { }, ); }).catch((e) => { + 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) => { - logger.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; diff --git a/src/errors.js b/src/errors.js index 409fafd9c..f3d88d643 100644 --- a/src/errors.js +++ b/src/errors.js @@ -1,6 +1,6 @@ // can't just do InvalidStoreError extends Error // because of http://babeljs.io/docs/usage/caveats/#classes -function InvalidStoreError(reason, value) { +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]); @@ -22,4 +22,25 @@ InvalidStoreError.prototype = Object.create(Error.prototype, { }); Reflect.setPrototypeOf(InvalidStoreError, Error); -module.exports.InvalidStoreError = InvalidStoreError; + +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); From 85a4a594c50e703ca8d97e1a3620aa50787957f0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 22 Nov 2018 16:46:25 +0000 Subject: [PATCH 454/472] Prepare changelog for v0.14.1 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e833882e5..83100bc8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +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) From 76c675cd09685664bc63883a881eed8808b3911f Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 22 Nov 2018 16:46:26 +0000 Subject: [PATCH 455/472] v0.14.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c686ad16c..8efd6514c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.14.0", + "version": "0.14.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From 4e66a2d436e0e79043eb5c06acf16bf585e361bd Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 28 Nov 2018 15:53:13 +0000 Subject: [PATCH 456/472] Don't re-establish sessions with unknown devices as it won't work --- src/crypto/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/crypto/index.js b/src/crypto/index.js index bcc5105e7..d36f449aa 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1453,19 +1453,26 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) { ); return; } - this._lastNewSessionForced[sender][deviceKey] = Date.now(); // establish a new olm session with this device since we're failing to decrypt messages // on a current session. // Note that an undecryptable message from another device could easily be spoofed - // is there anything we can do to mitigate this? const device = this._deviceList.getDeviceByIdentityKey(algorithm, deviceKey); + if (!device) { + logger.info( + "Couldn't find device for identity key " + deviceKey + + ": not re-establishing session", + ); + } const devicesByUser = {}; devicesByUser[sender] = [device]; await olmlib.ensureOlmSessionsForDevices( this._olmDevice, this._baseApis, devicesByUser, true, ); + this._lastNewSessionForced[sender][deviceKey] = Date.now(); + // Now send a blank message on that session so the other side knows about it. // (The keyshare request is sent in the clear so that won't do) // We send this first such that, as long as the toDevice messages arrive in the From 6db8dd620d5d9fda6bcc33165c0efcb325d0b2f8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 28 Nov 2018 16:11:28 +0000 Subject: [PATCH 457/472] Fix https://github.com/matrix-org/matrix-js-sdk/pull/792 --- src/crypto/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/crypto/index.js b/src/crypto/index.js index d36f449aa..a52122788 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -1464,6 +1464,7 @@ Crypto.prototype._onToDeviceBadEncrypted = async function(event) { "Couldn't find device for identity key " + deviceKey + ": not re-establishing session", ); + return; } const devicesByUser = {}; devicesByUser[sender] = [device]; From f8ea019f02c48c8b0b64fed25179a15b5e895a1f Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Thu, 29 Nov 2018 11:52:19 -0500 Subject: [PATCH 458/472] fix some assertions in e2e backup unit test --- spec/unit/crypto/backup.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index a654312b7..9f9927836 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -267,7 +267,7 @@ describe("MegolmBackup", function() { callback, method, path, queryParams, data, opts, ) { ++numCalls; - expect(numCalls <= 1); + 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")); @@ -372,7 +372,7 @@ describe("MegolmBackup", function() { callback, method, path, queryParams, data, opts, ) { ++numCalls; - expect(numCalls <= 2); + 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")); From e7c4a74ed667fb4653e1738a12c4f6f588a773b0 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 4 Dec 2018 01:27:08 +0000 Subject: [PATCH 459/472] Config should be called with auth --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 64bbf5082..723a6a064 100644 --- a/src/client.js +++ b/src/client.js @@ -767,7 +767,7 @@ MatrixClient.prototype.getGroups = function() { * @return {module:client.Promise} Resolves with an object containing the config. */ MatrixClient.prototype.getMediaConfig = function(callback) { - return this._http.requestWithPrefix( + return this._http.authedRequestWithPrefix( callback, "GET", "/config", undefined, undefined, httpApi.PREFIX_MEDIA_R0, ); }; From ec18df2c2ae3a2f870e4ed23b3ad31c03b2d4c9c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 Dec 2018 18:01:12 -0700 Subject: [PATCH 460/472] Move glob regex utilities out of the pushprocessor and into a more generic place --- src/pushprocessor.js | 23 +++-------------------- src/utils.js | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/pushprocessor.js b/src/pushprocessor.js index 1fdbdfe84..712c45399 100644 --- a/src/pushprocessor.js +++ b/src/pushprocessor.js @@ -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, }; @@ -244,22 +243,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; diff --git a/src/utils.js b/src/utils.js index 1587c64a3..ac3bd741e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -672,3 +672,27 @@ module.exports.removeHiddenChars = function(str) { return 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; +}; \ No newline at end of file From 84ab0fde51c67e22bb441297eb8b354dcac62571 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 5 Dec 2018 18:13:28 -0700 Subject: [PATCH 461/472] Appease the linter --- src/utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils.js b/src/utils.js index ac3bd741e..8ee81ed3e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -688,11 +688,11 @@ module.exports.globToRegexp = function(glob, extended) { pat = pat.replace(/\\\*/g, '.*'); pat = pat.replace(/\?/g, '.'); if (extended) { - pat = pat.replace(/\\\[(!|)(.*)\\]/g, function (match, p1, p2, offset, string) { + pat = pat.replace(/\\\[(!|)(.*)\\]/g, function(match, p1, p2, offset, string) { const first = p1 && '^' || ''; const second = p2.replace(/\\\-/, '-'); return '[' + first + second + ']'; }); } return pat; -}; \ No newline at end of file +}; From 586b0108117f29c04f2ebb2f96467a07a6f00648 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Dec 2018 11:11:28 +0000 Subject: [PATCH 462/472] Prepare changelog for v0.14.2-rc.1 --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83100bc8d..60b08db57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +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) From bb6ade216531d34dea5f520357c1a54e5606b46c Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 6 Dec 2018 11:11:29 +0000 Subject: [PATCH 463/472] v0.14.2-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5c7c49253..2f753bac0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.14.1", + "version": "0.14.2-rc.1", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From 1c4082af4513dca1a97032ff10a41c5d56d22166 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Dec 2018 13:31:37 +0000 Subject: [PATCH 464/472] Prepare changelog for v0.14.2 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60b08db57..4185005ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +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) From 57f6b0af0905368a21e8c549c3f78eb75b81797d Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 10 Dec 2018 13:31:37 +0000 Subject: [PATCH 465/472] v0.14.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2f753bac0..03fdc40ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "0.14.2-rc.1", + "version": "0.14.2", "description": "Matrix Client-Server SDK for Javascript", "main": "index.js", "scripts": { From 848e6e58975f1c39fae2050631846fbf3ae7bed0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 11 Dec 2018 21:41:15 -0700 Subject: [PATCH 466/472] Support reading custom status messages Part of https://github.com/vector-im/riot-web/issues/1528 --- src/models/room-state.js | 10 ++++++++++ src/models/user.js | 14 ++++++++++++++ src/sync.js | 10 ++++++++++ 3 files changed, 34 insertions(+) diff --git a/src/models/room-state.js b/src/models/room-state.js index ac5e20077..3dbd7ff88 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -154,6 +154,16 @@ RoomState.prototype.getMembers = function() { return utils.values(this.members); }; +/** + * Get all RoomMembers in this room, excluding the user IDs provided. + * @param {Array} excludedIds The user IDs to exclude. + * @return {Array} 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. diff --git a/src/models/user.js b/src/models/user.js index dbfaeb791..f4b5f7a26 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -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} 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.statusMessage = ""; this.displayName = userId; this.rawDisplayName = userId; this.avatarUrl = null; @@ -179,6 +183,16 @@ User.prototype.getLastActiveTs = function() { return this.lastPresenceTs - this.lastActiveAgo; }; +/** + * Manually set the user's status message. + * @param {MatrixEvent} event The im.vector.user_status event. + */ +User.prototype.updateStatusMessage = function(event) { + if (!event.getContent()) this.statusMessage = ""; + else this.statusMessage = event.getContent()["status"]; + this._updateModifiedTime(); +}; + /** * The User class. */ diff --git a/src/sync.js b/src/sync.js index 1ce75ba82..9d8c9a571 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1172,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.updateStatusMessage(e); + } else { + user = createNewUser(client, e.getStateKey()); + user.updateStatusMessage(e); + client.store.storeUser(user); + } + } } await Promise.mapSeries(stateEvents, processRoomEvent); From c4452909e7f06dfb4ba4f4d8e56e5814af215527 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 Dec 2018 08:38:05 -0700 Subject: [PATCH 467/472] 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 --- spec/unit/autodiscovery.spec.js | 670 ++++++++++++++++++++++++++++++++ src/autodiscovery.js | 390 +++++++++++++++++++ src/matrix.js | 2 + 3 files changed, 1062 insertions(+) create mode 100644 spec/unit/autodiscovery.spec.js create mode 100644 src/autodiscovery.js diff --git a/spec/unit/autodiscovery.spec.js b/spec/unit/autodiscovery.spec.js new file mode 100644 index 000000000..9928e098b --- /dev/null +++ b/spec/unit/autodiscovery.spec.js @@ -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); + }), + ]); + }); +}); diff --git a/src/autodiscovery.js b/src/autodiscovery.js new file mode 100644 index 000000000..6ce4392e2 --- /dev/null +++ b/src/autodiscovery.js @@ -0,0 +1,390 @@ +/* +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} 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} 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, + }); + } + }, + ); + }); + } +} diff --git a/src/matrix.js b/src/matrix.js index 47791472e..43d1a1c24 100644 --- a/src/matrix.js +++ b/src/matrix.js @@ -67,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 = From fb65c7f4bab0ad3764657b3f4c67ab93093343f6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 Dec 2018 13:21:13 -0700 Subject: [PATCH 468/472] Support setting status message in rooms that look like 1:1s Part of https://github.com/vector-im/riot-web/issues/1528 --- src/client.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/client.js b/src/client.js index 22cae3e87..9b5726f2f 100644 --- a/src/client.js +++ b/src/client.js @@ -2262,6 +2262,27 @@ MatrixClient.prototype.mxcUrlToHttp = ); }; +/** + * Sets a new status message for the user. The message may be null/falsey + * to clear the message. + * @param {string} newMessage The new message to set. + * @return {module:client.Promise} Resolves: to nothing + * @return {module:http-api.MatrixError} Rejects: with an error response. + */ +MatrixClient.prototype.setStatusMessage = function(newMessage) { + return Promise.all(this.getRooms().map((room) => { + const isJoined = room.getMyMembership() === "join"; + const looksLikeDm = room.getInvitedAndJoinedMemberCount() === 2; + if (isJoined && looksLikeDm) { + return this.sendStateEvent(room.roomId, "im.vector.user_status", { + status: newMessage, + }, this.getUserId()); + } else { + return Promise.resolve(); + } + })); +}; + /** * @param {Object} opts Options to apply * @param {string} opts.presence One of "online", "offline" or "unavailable" From 6cf956344148ce007c9c25fed52ada5ae36fc8e9 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 12 Dec 2018 10:52:35 +0000 Subject: [PATCH 469/472] Avoid checking key backup status if guest Fixes vector-im/riot-web#7839. Signed-off-by: J. Ryan Stinnett --- spec/unit/crypto.spec.js | 1 + src/crypto/index.js | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js index e3cabbac6..4a752cce6 100644 --- a/spec/unit/crypto.spec.js +++ b/spec/unit/crypto.spec.js @@ -70,6 +70,7 @@ describe("Crypto", function() { mockBaseApis = { sendToDevice: expect.createSpy(), getKeyBackupVersion: expect.createSpy(), + isGuest: expect.createSpy(), }; mockRoomList = {}; diff --git a/src/crypto/index.js b/src/crypto/index.js index a52122788..17abf8933 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -212,6 +212,11 @@ Crypto.prototype.init = async function() { */ Crypto.prototype._checkAndStartKeyBackup = async function() { console.log("Checking key backup status..."); + if (this._baseApis.isGuest()) { + console.log("Skipping key backup check since user is guest"); + this._checkedForBackup = true; + return; + } let backupInfo; try { backupInfo = await this._baseApis.getKeyBackupVersion(); From 08b3dfa3b5b5d0b63272f0b80b9fdd88d0795c45 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 12 Dec 2018 23:05:03 -0700 Subject: [PATCH 470/472] 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. --- src/client.js | 2 +- src/models/user.js | 8 ++++---- src/sync.js | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/client.js b/src/client.js index 9b5726f2f..c942acb9d 100644 --- a/src/client.js +++ b/src/client.js @@ -2269,7 +2269,7 @@ MatrixClient.prototype.mxcUrlToHttp = * @return {module:client.Promise} Resolves: to nothing * @return {module:http-api.MatrixError} Rejects: with an error response. */ -MatrixClient.prototype.setStatusMessage = function(newMessage) { +MatrixClient.prototype._unstable_setStatusMessage = function(newMessage) { return Promise.all(this.getRooms().map((room) => { const isJoined = room.getMyMembership() === "join"; const looksLikeDm = room.getInvitedAndJoinedMemberCount() === 2; diff --git a/src/models/user.js b/src/models/user.js index f4b5f7a26..d1496c996 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -49,7 +49,7 @@ function User(userId) { this.userId = userId; this.presence = "offline"; this.presenceStatusMsg = null; - this.statusMessage = ""; + this._unstable_statusMessage = ""; this.displayName = userId; this.rawDisplayName = userId; this.avatarUrl = null; @@ -187,9 +187,9 @@ User.prototype.getLastActiveTs = function() { * Manually set the user's status message. * @param {MatrixEvent} event The im.vector.user_status event. */ -User.prototype.updateStatusMessage = function(event) { - if (!event.getContent()) this.statusMessage = ""; - else this.statusMessage = event.getContent()["status"]; +User.prototype._unstable_updateStatusMessage = function(event) { + if (!event.getContent()) this._unstable_statusMessage = ""; + else this._unstable_statusMessage = event.getContent()["status"]; this._updateModifiedTime(); }; diff --git a/src/sync.js b/src/sync.js index 9d8c9a571..f4ea0bf88 100644 --- a/src/sync.js +++ b/src/sync.js @@ -1175,10 +1175,10 @@ SyncApi.prototype._processSyncResponse = async function( if (e.isState() && e.getType() === "im.vector.user_status") { let user = client.store.getUser(e.getStateKey()); if (user) { - user.updateStatusMessage(e); + user._unstable_updateStatusMessage(e); } else { user = createNewUser(client, e.getStateKey()); - user.updateStatusMessage(e); + user._unstable_updateStatusMessage(e); client.store.storeUser(user); } } From 5e3f42ec5a382baa6be9adfef9e4087967c32d32 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 13 Dec 2018 01:05:56 +0000 Subject: [PATCH 471/472] 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 --- spec/unit/crypto.spec.js | 3 +++ spec/unit/crypto/algorithms/megolm.spec.js | 6 +----- spec/unit/crypto/algorithms/olm.spec.js | 6 +----- spec/unit/crypto/backup.spec.js | 7 ++----- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/spec/unit/crypto.spec.js b/spec/unit/crypto.spec.js index 4a752cce6..522060e36 100644 --- a/spec/unit/crypto.spec.js +++ b/spec/unit/crypto.spec.js @@ -1,4 +1,7 @@ import 'source-map-support/register'; + +import '../olm-loader'; + import Crypto from '../../lib/crypto'; import expect from 'expect'; diff --git a/spec/unit/crypto/algorithms/megolm.spec.js b/spec/unit/crypto/algorithms/megolm.spec.js index 641adb19c..164158e90 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.js +++ b/spec/unit/crypto/algorithms/megolm.spec.js @@ -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'; diff --git a/spec/unit/crypto/algorithms/olm.spec.js b/spec/unit/crypto/algorithms/olm.spec.js index 46fcff38b..7b6bc626a 100644 --- a/spec/unit/crypto/algorithms/olm.spec.js +++ b/spec/unit/crypto/algorithms/olm.spec.js @@ -14,11 +14,7 @@ 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 megolm tests: libolm not available"); -} +import '../../../olm-loader'; import expect from 'expect'; import WebStorageSessionStore from '../../../../lib/store/session/webstorage'; diff --git a/spec/unit/crypto/backup.spec.js b/spec/unit/crypto/backup.spec.js index 9f9927836..1bccb5a74 100644 --- a/spec/unit/crypto/backup.spec.js +++ b/spec/unit/crypto/backup.spec.js @@ -13,11 +13,8 @@ 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 megolm backup tests: libolm not available"); -} + +import '../../olm-loader'; import expect from 'expect'; import Promise from 'bluebird'; From 06bc6e7568403cf9d391636fa09d3256649ed918 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 13 Dec 2018 09:42:22 -0700 Subject: [PATCH 472/472] Update jsdoc to match _unstable_ prefix --- src/models/user.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/user.js b/src/models/user.js index d1496c996..ec2f495af 100644 --- a/src/models/user.js +++ b/src/models/user.js @@ -39,7 +39,7 @@ 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} statusMessage The status message for the user, if known. This is + * @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.