You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-12-01 04:43:29 +03:00
Merge remote-tracking branch 'origin/develop' into uhoreg-e2e_backups
This commit is contained in:
92
CHANGELOG.md
92
CHANGELOG.md
@@ -1,15 +1,93 @@
|
||||
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.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (TDB)
|
||||
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.10.6...v0.11.0)
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.0-rc.1...v0.11.0)
|
||||
|
||||
BREAKING CHANGES
|
||||
----------------
|
||||
* v0.11.0-rc.1 introduced some breaking changes - see the respective release notes.
|
||||
|
||||
No changes since rc.1
|
||||
|
||||
Changes in [0.11.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0-rc.1) (2018-09-07)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.9...v0.11.0-rc.1)
|
||||
|
||||
* Support for lazy loading members. This should improve performance for
|
||||
users who joined big rooms a lot. Pass to `lazyLoadMembers = true` option when calling `startClient`.
|
||||
|
||||
BREAKING CHANGES
|
||||
----------------
|
||||
|
||||
* `MatrixClient::startClient` now returns a Promise. No method should be called on the client before that promise resolves. Before this method didn't return anything.
|
||||
* A new `CATCHUP` sync state, emitted by `MatrixClient#"sync"` and returned by `MatrixClient::getSyncState()`, when doing initial sync after the `ERROR` state. See `MatrixClient` documentation for details.
|
||||
* `RoomState::maySendEvent('m.room.message', userId)` & `RoomState::maySendMessage(userId)` do not check the membership of the user anymore, only the power level. To check if the syncing user is allowed to write in a room, use `Room::maySendMessage()` as `RoomState` is not always aware of the syncing user's membership anymore, in case lazy loading of members is enabled.
|
||||
|
||||
All Changes
|
||||
-----------
|
||||
|
||||
* Only emit CATCHUP if recovering from conn error
|
||||
[\#727](https://github.com/matrix-org/matrix-js-sdk/pull/727)
|
||||
* Fix docstring for sync data.error
|
||||
[\#725](https://github.com/matrix-org/matrix-js-sdk/pull/725)
|
||||
* Re-apply "Don't rely on members to query if syncing user can post to room"
|
||||
[\#723](https://github.com/matrix-org/matrix-js-sdk/pull/723)
|
||||
* Revert "Don't rely on members to query if syncing user can post to room"
|
||||
[\#721](https://github.com/matrix-org/matrix-js-sdk/pull/721)
|
||||
* Don't rely on members to query if syncing user can post to room
|
||||
[\#717](https://github.com/matrix-org/matrix-js-sdk/pull/717)
|
||||
* Fixes for room.guessDMUserId
|
||||
[\#719](https://github.com/matrix-org/matrix-js-sdk/pull/719)
|
||||
* Fix filepanel also filtering main timeline with LL turned on.
|
||||
[\#716](https://github.com/matrix-org/matrix-js-sdk/pull/716)
|
||||
* Remove lazy loaded members when leaving room
|
||||
[\#711](https://github.com/matrix-org/matrix-js-sdk/pull/711)
|
||||
* Fix: show spinner again while recovering from connection error
|
||||
[\#702](https://github.com/matrix-org/matrix-js-sdk/pull/702)
|
||||
* Add method to query LL state in client
|
||||
[\#714](https://github.com/matrix-org/matrix-js-sdk/pull/714)
|
||||
* Fix: also load invited members when lazy loading members
|
||||
[\#707](https://github.com/matrix-org/matrix-js-sdk/pull/707)
|
||||
* Pass through function to discard megolm session
|
||||
[\#704](https://github.com/matrix-org/matrix-js-sdk/pull/704)
|
||||
|
||||
Changes in [0.10.9](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.9) (2018-09-03)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.9-rc.2...v0.10.9)
|
||||
|
||||
* No changes since rc.2
|
||||
|
||||
Changes in [0.10.9-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.9-rc.2) (2018-08-31)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.9-rc.1...v0.10.9-rc.2)
|
||||
|
||||
* Fix for "otherMember.getAvatarUrl is not a function"
|
||||
[\#708](https://github.com/matrix-org/matrix-js-sdk/pull/708)
|
||||
|
||||
Changes in [0.10.9-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.9-rc.1) (2018-08-30)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.8...v0.10.9-rc.1)
|
||||
|
||||
* Fix DM avatar
|
||||
[\#706](https://github.com/matrix-org/matrix-js-sdk/pull/706)
|
||||
* Lazy loading: avoid loading members at initial sync for e2e rooms
|
||||
[\#699](https://github.com/matrix-org/matrix-js-sdk/pull/699)
|
||||
* Improve setRoomEncryption guard against multiple m.room.encryption st…
|
||||
[\#700](https://github.com/matrix-org/matrix-js-sdk/pull/700)
|
||||
* Revert "Lazy loading: don't block on setting up room crypto"
|
||||
[\#698](https://github.com/matrix-org/matrix-js-sdk/pull/698)
|
||||
* Lazy loading: don't block on setting up room crypto
|
||||
[\#696](https://github.com/matrix-org/matrix-js-sdk/pull/696)
|
||||
* Add getVisibleRooms()
|
||||
[\#695](https://github.com/matrix-org/matrix-js-sdk/pull/695)
|
||||
* Add wrapper around getJoinedMemberCount()
|
||||
[\#697](https://github.com/matrix-org/matrix-js-sdk/pull/697)
|
||||
* Api to fetch events via /room/.../event/..
|
||||
[\#694](https://github.com/matrix-org/matrix-js-sdk/pull/694)
|
||||
* Support for room upgrades
|
||||
[\#693](https://github.com/matrix-org/matrix-js-sdk/pull/693)
|
||||
* Lazy loading of room members
|
||||
[\#691](https://github.com/matrix-org/matrix-js-sdk/pull/691)
|
||||
|
||||
Changes in [0.10.8](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.10.8) (2018-08-20)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.8-rc.1...v0.10.8)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.10.8",
|
||||
"version": "0.11.0",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
@@ -1397,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");
|
||||
@@ -1408,4 +1411,40 @@ describe("Room", function() {
|
||||
expect(room.getMyMembership()).toEqual("join");
|
||||
});
|
||||
});
|
||||
|
||||
describe("guessDMUserId", function() {
|
||||
it("should return first hero id",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.setSummary({'m.heroes': [userB]});
|
||||
expect(room.guessDMUserId()).toEqual(userB);
|
||||
});
|
||||
it("should return first member that isn't self",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.addLiveEvents([utils.mkMembership({
|
||||
user: userB, mship: "join",
|
||||
room: roomId, event: true,
|
||||
})]);
|
||||
expect(room.guessDMUserId()).toEqual(userB);
|
||||
});
|
||||
it("should return self if only member present",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
expect(room.guessDMUserId()).toEqual(userA);
|
||||
});
|
||||
});
|
||||
|
||||
describe("maySendMessage", function() {
|
||||
it("should return false if synced membership not join",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.setSyncedMembership("invite");
|
||||
expect(room.maySendMessage()).toEqual(false);
|
||||
room.setSyncedMembership("leave");
|
||||
expect(room.maySendMessage()).toEqual(false);
|
||||
room.setSyncedMembership("join");
|
||||
expect(room.maySendMessage()).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -703,6 +703,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 {string} 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");
|
||||
}
|
||||
this._crypto.forceDiscardSession(roomId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list containing all of the room keys
|
||||
*
|
||||
@@ -2318,7 +2333,9 @@ function(roomId, fromToken, limit, dir, timelineFilter = undefined) {
|
||||
|
||||
let filter = null;
|
||||
if (this._clientOpts.lazyLoadMembers) {
|
||||
filter = LAZY_LOADING_MESSAGES_FILTER;
|
||||
// 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) {
|
||||
// XXX: it's horrific that /messages' filter parameter doesn't match
|
||||
@@ -3286,6 +3303,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) {
|
||||
@@ -3343,6 +3364,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
|
||||
@@ -3688,6 +3717,12 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
|
||||
* a state of SYNCING. <i>This is the equivalent of "syncComplete" in the
|
||||
* previous API.</i></li>
|
||||
*
|
||||
* <li>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.</li>
|
||||
*
|
||||
* <li>SYNCING : The client is currently polling for new events from the server.
|
||||
* This will be called <i>after</i> processing latest events from a sync.</li>
|
||||
*
|
||||
@@ -3712,10 +3747,10 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
|
||||
* |
|
||||
* +----->PREPARED -------> SYNCING <--+
|
||||
* | ^ | ^ |
|
||||
* | | | | |
|
||||
* | | V | |
|
||||
* null ------+ | +--------RECONNECTING |
|
||||
* | | V |
|
||||
* | CATCHUP ----------+ | | |
|
||||
* | ^ V | |
|
||||
* null ------+ | +------- RECONNECTING |
|
||||
* | V V |
|
||||
* +------->ERROR ---------------------+
|
||||
*
|
||||
* NB: 'null' will never be emitted by this event.
|
||||
@@ -3765,7 +3800,7 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
|
||||
*
|
||||
* @param {?Object} data Data about this transition.
|
||||
*
|
||||
* @param {MatrixError} data.err The matrix error if <code>state=ERROR</code>.
|
||||
* @param {MatrixError} data.error The matrix error if <code>state=ERROR</code>.
|
||||
*
|
||||
* @param {String} data.oldSyncToken The 'since' token passed to /sync.
|
||||
* <code>null</code> for the first successful sync since this client was
|
||||
|
||||
@@ -71,6 +71,9 @@ export default class RoomList {
|
||||
}
|
||||
|
||||
async setRoomEncryption(roomId, roomInfo) {
|
||||
// important that this happens before calling into the store
|
||||
// as it prevents the Crypto::setRoomEncryption from calling
|
||||
// this twice for consecutive m.room.encryption events
|
||||
this._roomEncryption[roomId] = roomInfo;
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
|
||||
|
||||
@@ -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.
|
||||
@@ -488,6 +489,8 @@ MegolmEncryption.prototype.encryptMessage = function(room, eventType, content) {
|
||||
session_id: session.sessionId,
|
||||
// Include our device ID so that recipients can send us a
|
||||
// m.new_device message if they don't have our session key.
|
||||
// XXX: Do we still need this now that m.new_device messages
|
||||
// no longer exist since #483?
|
||||
device_id: self._deviceId,
|
||||
};
|
||||
|
||||
@@ -496,6 +499,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
|
||||
@@ -550,12 +563,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) {
|
||||
|
||||
@@ -111,6 +111,15 @@ 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.
|
||||
this._roomDeviceTrackingState = {};
|
||||
}
|
||||
utils.inherits(Crypto, EventEmitter);
|
||||
|
||||
@@ -172,6 +181,12 @@ 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
|
||||
@@ -611,6 +626,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 {string} 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).
|
||||
*
|
||||
@@ -619,25 +651,49 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
|
||||
* @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)
|
||||
* 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 we already have encryption in this room, we should ignore this event
|
||||
// (for now at least. maybe we should alert the user somehow?)
|
||||
// if state is being replayed from storage, we might already have a configuration
|
||||
// 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 && JSON.stringify(existingConfig) != JSON.stringify(config)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
// 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) {
|
||||
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,
|
||||
@@ -649,23 +705,58 @@ 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}`);
|
||||
if (storeConfigPromise) {
|
||||
await storeConfigPromise;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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 trackMembers = 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);
|
||||
});
|
||||
if (!inhibitDeviceQuery) {
|
||||
this._deviceList.refreshOutdatedDeviceLists();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
let promise = this._roomDeviceTrackingState[roomId];
|
||||
if (!promise) {
|
||||
promise = trackMembers();
|
||||
this._roomDeviceTrackingState[roomId] = promise;
|
||||
}
|
||||
return promise;
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {Object} module:crypto~OlmSessionResult
|
||||
@@ -757,7 +848,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.
|
||||
*
|
||||
@@ -768,7 +859,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");
|
||||
}
|
||||
@@ -786,6 +878,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
|
||||
@@ -796,9 +894,9 @@ Crypto.prototype.encryptEvent = function(event, room) {
|
||||
delete content['m.relates_to'];
|
||||
}
|
||||
|
||||
return alg.encryptMessage(
|
||||
room, event.getType(), content,
|
||||
).then((encryptedContent) => {
|
||||
const encryptedContent = await alg.encryptMessage(
|
||||
room, event.getType(), content);
|
||||
|
||||
if (mRelatesTo) {
|
||||
encryptedContent['m.relates_to'] = mRelatesTo;
|
||||
}
|
||||
@@ -809,7 +907,6 @@ Crypto.prototype.encryptEvent = function(event, room) {
|
||||
this._olmDevice.deviceCurve25519Key,
|
||||
this._olmDevice.deviceEd25519Key,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -924,6 +1021,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 = {};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -969,11 +1067,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)) {
|
||||
@@ -985,12 +1084,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);
|
||||
@@ -1000,17 +1100,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();
|
||||
@@ -1079,7 +1183,11 @@ 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
|
||||
@@ -1089,6 +1197,7 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) {
|
||||
console.log('Invite event for ' + member.userId + ' in ' + roomId);
|
||||
this._deviceList.startTrackingDeviceList(member.userId);
|
||||
}
|
||||
}
|
||||
|
||||
alg.onRoomMembership(event, member, oldMembership);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -495,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);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -651,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;
|
||||
@@ -663,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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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");
|
||||
@@ -279,14 +280,84 @@ 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];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Assuming this room is a DM room, tries to guess with which user.
|
||||
* @return {string} user id of the other member (could be syncing user)
|
||||
*/
|
||||
Room.prototype.guessDMUserId = function() {
|
||||
const me = this.getMember(this.myUserId);
|
||||
if (me) {
|
||||
const inviterId = me.getDMInviter();
|
||||
if (inviterId) {
|
||||
return inviterId;
|
||||
}
|
||||
}
|
||||
// remember, we're assuming this room is a DM,
|
||||
// so returning the first member we find should be fine
|
||||
const hasHeroes = Array.isArray(this._summaryHeroes) &&
|
||||
this._summaryHeroes.length;
|
||||
if (hasHeroes) {
|
||||
return this._summaryHeroes[0];
|
||||
}
|
||||
const members = this.currentState.getMembers();
|
||||
const anyMember = members.find((m) => m.userId !== this.myUserId);
|
||||
if (anyMember) {
|
||||
return anyMember.userId;
|
||||
}
|
||||
// it really seems like I'm the only user in the room
|
||||
// so I probably created a room with just me in it
|
||||
// and marked it as a DM. Ok then
|
||||
return this.myUserId;
|
||||
};
|
||||
|
||||
Room.prototype.getAvatarFallbackMember = function() {
|
||||
const memberCount = this.getInvitedAndJoinedMemberCount();
|
||||
if (memberCount > 2) {
|
||||
return;
|
||||
}
|
||||
const hasHeroes = Array.isArray(this._summaryHeroes) &&
|
||||
this._summaryHeroes.length;
|
||||
if (hasHeroes) {
|
||||
const availableMember = this._summaryHeroes.map((userId) => {
|
||||
return this.getMember(userId);
|
||||
}).find((member) => !!member);
|
||||
if (availableMember) {
|
||||
return availableMember;
|
||||
}
|
||||
}
|
||||
const members = this.currentState.getMembers();
|
||||
// could be different than memberCount
|
||||
// as this includes left members
|
||||
if (members.length <= 2) {
|
||||
const availableMember = members.find((m) => {
|
||||
return m.userId !== this.myUserId;
|
||||
});
|
||||
if (availableMember) {
|
||||
return availableMember;
|
||||
}
|
||||
}
|
||||
// if all else fails, try falling back to a user,
|
||||
// and create a one-off member for it
|
||||
if (hasHeroes) {
|
||||
const availableUser = this._summaryHeroes.map((userId) => {
|
||||
return this._client.getUser(userId);
|
||||
}).find((user) => !!user);
|
||||
if (availableUser) {
|
||||
const member = new RoomMember(
|
||||
this.roomId, availableUser.userId);
|
||||
member.user = availableUser;
|
||||
return member;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the membership this room was received as during sync
|
||||
* @param {string} membership join | leave | invite
|
||||
@@ -298,7 +369,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,
|
||||
});
|
||||
@@ -343,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())
|
||||
@@ -361,16 +444,40 @@ 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;
|
||||
this._membersPromise = inMemoryUpdate;
|
||||
|
||||
return this._membersPromise;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the lazily loaded members from storage if needed
|
||||
*/
|
||||
Room.prototype.clearLoadedMembersIfNeeded = async function() {
|
||||
if (this._opts.lazyLoadMembers && this._membersPromise) {
|
||||
await this.loadMembersIfNeeded();
|
||||
await this._client.store.clearOutOfBandMembers(this.roomId);
|
||||
this.currentState.clearOutOfBandMembers();
|
||||
this._membersPromise = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* called when sync receives this room in the leave section
|
||||
* to do cleanup after leaving a room. Possibly called multiple times.
|
||||
*/
|
||||
Room.prototype.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.
|
||||
*
|
||||
@@ -485,8 +592,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;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -644,6 +756,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.
|
||||
@@ -1373,6 +1493,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.
|
||||
|
||||
@@ -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`,
|
||||
);
|
||||
@@ -254,22 +265,14 @@ 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: 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 = 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);
|
||||
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.
|
||||
@@ -281,15 +284,47 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
oob_written: true,
|
||||
state_key: 0,
|
||||
};
|
||||
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(() => {
|
||||
store.put(markerObject);
|
||||
await txnAsPromise(tx);
|
||||
console.log(`LL: backend done storing for ${roomId}!`);
|
||||
});
|
||||
},
|
||||
|
||||
clearOutOfBandMembers: async function(roomId) {
|
||||
// the approach to delete all members for a room
|
||||
// is to get the min and max state key from the index
|
||||
// for that room, and then delete between those
|
||||
// keys in the store.
|
||||
// this should be way faster than deleting every member
|
||||
// individually for a large room.
|
||||
const readTx = this.db.transaction(
|
||||
["oob_membership_events"],
|
||||
"readonly");
|
||||
const store = readTx.objectStore("oob_membership_events");
|
||||
const roomIndex = store.index("room");
|
||||
const roomRange = IDBKeyRange.only(roomId);
|
||||
|
||||
const minStateKeyProm = reqAsCursorPromise(
|
||||
roomIndex.openKeyCursor(roomRange, "next"),
|
||||
).then((cursor) => cursor && cursor.primaryKey[1]);
|
||||
const maxStateKeyProm = reqAsCursorPromise(
|
||||
roomIndex.openKeyCursor(roomRange, "prev"),
|
||||
).then((cursor) => cursor && cursor.primaryKey[1]);
|
||||
const [minStateKey, maxStateKey] = await Promise.all(
|
||||
[minStateKeyProm, maxStateKeyProm]);
|
||||
|
||||
const writeTx = this.db.transaction(
|
||||
["oob_membership_events"],
|
||||
"readwrite");
|
||||
const writeStore = writeTx.objectStore("oob_membership_events");
|
||||
const membersKeyRange = IDBKeyRange.bound(
|
||||
[roomId, minStateKey],
|
||||
[roomId, maxStateKey],
|
||||
);
|
||||
|
||||
console.log(`LL: Deleting all users + marker in storage for ` +
|
||||
`room ${roomId}, with key range:`,
|
||||
[roomId, minStateKey], [roomId, maxStateKey]);
|
||||
await reqAsPromise(writeStore.delete(membersKeyRange));
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -389,7 +424,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
roomsData: roomsData,
|
||||
groupsData: groupsData,
|
||||
}); // put == UPSERT
|
||||
return promiseifyTxn(txn);
|
||||
return txnAsPromise(txn);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -406,7 +441,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 +463,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
event: tuple[1],
|
||||
}); // put == UPSERT
|
||||
}
|
||||
return promiseifyTxn(txn);
|
||||
return txnAsPromise(txn);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -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<Object[]>} A list of presence events in their raw form.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -272,6 +272,10 @@ StubStore.prototype = {
|
||||
setOutOfBandMembers: function() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
clearOutOfBandMembers: function() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
|
||||
/** Stub Store class. */
|
||||
|
||||
29
src/sync.js
29
src/sync.js
@@ -779,7 +779,18 @@ 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(() => {
|
||||
this._startKeepAlives().then((connDidFail) => {
|
||||
// Only emit CATCHUP if we detected a connectivity error: if we didn't,
|
||||
// it's quite likely the sync will fail again for the same reason and we
|
||||
// want to stay in ERROR rather than keep flip-flopping between ERROR
|
||||
// and CATCHUP.
|
||||
if (connDidFail && this.getSyncState() === 'ERROR') {
|
||||
this._updateSyncState("CATCHUP", {
|
||||
oldSyncToken: null,
|
||||
nextSyncToken: null,
|
||||
catchingUp: true,
|
||||
});
|
||||
}
|
||||
this._sync(syncOptions);
|
||||
});
|
||||
|
||||
@@ -1145,6 +1156,8 @@ SyncApi.prototype._processSyncResponse = async function(
|
||||
accountDataEvents.forEach(function(e) {
|
||||
client.emit("event", e);
|
||||
});
|
||||
|
||||
room.onLeft();
|
||||
});
|
||||
|
||||
// update the notification timeline, if appropriate.
|
||||
@@ -1217,13 +1230,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;
|
||||
}
|
||||
}
|
||||
@@ -1240,7 +1256,7 @@ SyncApi.prototype._pokeKeepAlive = function() {
|
||||
).done(function() {
|
||||
success();
|
||||
}, function(err) {
|
||||
if (err.httpStatus == 400) {
|
||||
if (err.httpStatus == 400 || err.httpStatus == 404) {
|
||||
// treat this as a success because the server probably just doesn't
|
||||
// support /versions: point is, we're getting a response.
|
||||
// We wait a short time though, just in case somehow the server
|
||||
@@ -1248,8 +1264,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
|
||||
@@ -1257,7 +1274,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 });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user