1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-28 05:03:59 +03:00

Merge remote-tracking branch 'origin/develop' into uhoreg-e2e_backups

This commit is contained in:
David Baker
2018-09-11 12:02:47 +01:00
17 changed files with 632 additions and 155 deletions

View File

@@ -1,15 +1,93 @@
BREAKING CHANGE Changes in [0.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (2018-09-10)
---------------
* `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)
================================================================================================== ==================================================================================================
[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 * 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`. 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) 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) [Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.10.8-rc.1...v0.10.8)

View File

@@ -1,6 +1,6 @@
{ {
"name": "matrix-js-sdk", "name": "matrix-js-sdk",
"version": "0.10.8", "version": "0.11.0",
"description": "Matrix Client-Server SDK for Javascript", "description": "Matrix Client-Server SDK for Javascript",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@@ -380,7 +380,7 @@ describe("MatrixClient", function() {
client.startClient(); client.startClient();
}); });
it("should transition ERROR -> PREPARED after /sync if prev failed", it("should transition ERROR -> CATCHUP after /sync if prev failed",
function(done) { function(done) {
const expectedStates = []; const expectedStates = [];
acceptKeepalives = false; acceptKeepalives = false;
@@ -403,7 +403,7 @@ describe("MatrixClient", function() {
expectedStates.push(["RECONNECTING", null]); expectedStates.push(["RECONNECTING", null]);
expectedStates.push(["ERROR", "RECONNECTING"]); expectedStates.push(["ERROR", "RECONNECTING"]);
expectedStates.push(["PREPARED", "ERROR"]); expectedStates.push(["CATCHUP", "ERROR"]);
client.on("sync", syncChecker(expectedStates, done)); client.on("sync", syncChecker(expectedStates, done));
client.startClient(); client.startClient();
}); });

View File

@@ -447,13 +447,6 @@ describe("RoomState", function() {
}); });
describe("maySendStateEvent", 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", it("should say any member may send state with no power level event",
function() { function() {
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true); expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
@@ -640,14 +633,6 @@ describe("RoomState", function() {
}); });
describe("maySendEvent", 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", it("should say any member may send events with no power level event",
function() { function() {
expect(state.maySendEvent('m.room.message', userA)).toEqual(true); expect(state.maySendEvent('m.room.message', userA)).toEqual(true);

View File

@@ -1318,6 +1318,9 @@ describe("Room", function() {
// events should already be MatrixEvents // events should already be MatrixEvents
return function(event) {return event;}; return function(event) {return event;};
}, },
isRoomEncrypted: function() {
return false;
},
_http: { _http: {
serverResponse, serverResponse,
authedRequest: function() { authedRequest: function() {
@@ -1397,7 +1400,7 @@ describe("Room", function() {
describe("getMyMembership", function() { describe("getMyMembership", function() {
it("should return synced membership if membership isn't available yet", it("should return synced membership if membership isn't available yet",
async function() { function() {
const room = new Room(roomId, null, userA); const room = new Room(roomId, null, userA);
room.setSyncedMembership("invite"); room.setSyncedMembership("invite");
expect(room.getMyMembership()).toEqual("invite"); expect(room.getMyMembership()).toEqual("invite");
@@ -1408,4 +1411,40 @@ describe("Room", function() {
expect(room.getMyMembership()).toEqual("join"); 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);
});
});
}); });

View File

@@ -703,6 +703,21 @@ MatrixClient.prototype.isRoomEncrypted = function(roomId) {
return this._roomList.isRoomEncrypted(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 * Get a list containing all of the room keys
* *
@@ -2318,7 +2333,9 @@ function(roomId, fromToken, limit, dir, timelineFilter = undefined) {
let filter = null; let filter = null;
if (this._clientOpts.lazyLoadMembers) { 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) { if (timelineFilter) {
// XXX: it's horrific that /messages' filter parameter doesn't match // 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.crypto = this._crypto;
opts.canResetEntireTimeline = (roomId) => { opts.canResetEntireTimeline = (roomId) => {
if (!this._canResetTimelineCallback) { if (!this._canResetTimelineCallback) {
@@ -3343,6 +3364,14 @@ MatrixClient.prototype.doesServerSupportLazyLoading = async function() {
return this._serverSupportsLazyLoading; 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. * 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 * 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 * a state of SYNCING. <i>This is the equivalent of "syncComplete" in the
* previous API.</i></li> * 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. * <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> * This will be called <i>after</i> processing latest events from a sync.</li>
* *
@@ -3711,11 +3746,11 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
* +---->STOPPED * +---->STOPPED
* | * |
* +----->PREPARED -------> SYNCING <--+ * +----->PREPARED -------> SYNCING <--+
* | ^ | ^ | * | ^ | ^ |
* | | | | | * | CATCHUP ----------+ | | |
* | | V | | * | ^ V | |
* null ------+ | +--------RECONNECTING | * null ------+ | +------- RECONNECTING |
* | | V | * | V V |
* +------->ERROR ---------------------+ * +------->ERROR ---------------------+
* *
* NB: 'null' will never be emitted by this event. * 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 {?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. * @param {String} data.oldSyncToken The 'since' token passed to /sync.
* <code>null</code> for the first successful sync since this client was * <code>null</code> for the first successful sync since this client was

View File

@@ -71,6 +71,9 @@ export default class RoomList {
} }
async setRoomEncryption(roomId, roomInfo) { 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; this._roomEncryption[roomId] = roomInfo;
await this._cryptoStore.doTxn( await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => { 'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {

View File

@@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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, session_id: session.sessionId,
// Include our device ID so that recipients can send us a // Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key. // 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, 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 * 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 * 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 // 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 // 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 // with them, which means that they will have announced any new devices via
// an m.new_device. // device_lists in their /sync response. This cache should then be maintained
// // using all the device_lists changes and left fields.
// XXX: what if the cache is stale, and the user left the room we had in // See https://github.com/vector-im/riot-web/issues/2305 for details.
// common and then added new devices before joining this one? --Matthew
//
// yup, see https://github.com/vector-im/riot-web/issues/2305 --richvdh
const devices = await this._crypto.downloadKeys(roomMembers, false); const devices = await this._crypto.downloadKeys(roomMembers, false);
// remove any blocked devices // remove any blocked devices
for (const userId in devices) { for (const userId in devices) {

View File

@@ -111,6 +111,15 @@ function Crypto(baseApis, sessionStore, userId, deviceId,
this._receivedRoomKeyRequestCancellations = []; this._receivedRoomKeyRequestCancellations = [];
// true if we are currently processing received room key requests // true if we are currently processing received room key requests
this._processingRoomKeyRequests = false; 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); 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 * Tell the crypto module to register for MatrixClient events which it needs to
* listen for * listen for
@@ -611,6 +626,23 @@ Crypto.prototype.getEventSenderDeviceInfo = function(event) {
return device; 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). * 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 {object} config The encryption config for the room.
* *
* @param {boolean=} inhibitDeviceQuery true to suppress device list query for * @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) { Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDeviceQuery) {
// if we already have encryption in this room, we should ignore this event // if state is being replayed from storage, we might already have a configuration
// (for now at least. maybe we should alert the user somehow?) // 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); const existingConfig = this._roomList.getRoomEncryption(roomId);
if (existingConfig && JSON.stringify(existingConfig) != JSON.stringify(config)) { if (existingConfig) {
console.error("Ignoring m.room.encryption event which requests " + if (JSON.stringify(existingConfig) != JSON.stringify(config)) {
"a change of config in " + roomId); 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; 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]; const AlgClass = algorithms.ENCRYPTION_CLASSES[config.algorithm];
if (!AlgClass) { if (!AlgClass) {
throw new Error("Unable to encrypt with " + config.algorithm); throw new Error("Unable to encrypt with " + config.algorithm);
} }
await this._roomList.setRoomEncryption(roomId, config);
const alg = new AlgClass({ const alg = new AlgClass({
userId: this._userId, userId: this._userId,
deviceId: this._deviceId, deviceId: this._deviceId,
@@ -649,24 +705,59 @@ Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDevic
}); });
this._roomEncryptors[roomId] = alg; this._roomEncryptors[roomId] = alg;
// make sure we are tracking the device lists for all users in this room. if (storeConfigPromise) {
console.log("Enabling encryption in " + roomId + "; " + await storeConfigPromise;
"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(); if (!this._lazyLoadMembers) {
members.forEach((m) => { console.log("Enabling encryption in " + roomId + "; " +
this._deviceList.startTrackingDeviceList(m.userId); "starting to track device lists for all users therein");
});
if (!inhibitDeviceQuery) { await this.trackRoomDevices(roomId);
this._deviceList.refreshOutdatedDeviceLists(); // 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);
});
};
let promise = this._roomDeviceTrackingState[roomId];
if (!promise) {
promise = trackMembers();
this._roomDeviceTrackingState[roomId] = promise;
}
return promise;
};
/** /**
* @typedef {Object} module:crypto~OlmSessionResult * @typedef {Object} module:crypto~OlmSessionResult
* @property {module:crypto/deviceinfo} device device info * @property {module:crypto/deviceinfo} device device info
@@ -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. * 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 * @return {module:client.Promise?} Promise which resolves when the event has been
* encrypted, or null if nothing was needed * 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) { if (!room) {
throw new Error("Cannot send encrypted messages in unknown rooms"); 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(); let content = event.getContent();
// If event has an m.relates_to then we need // If event has an m.relates_to then we need
// to put this on the wrapping event instead // to put this on the wrapping event instead
@@ -796,20 +894,19 @@ Crypto.prototype.encryptEvent = function(event, room) {
delete content['m.relates_to']; delete content['m.relates_to'];
} }
return alg.encryptMessage( const encryptedContent = await alg.encryptMessage(
room, event.getType(), content, room, event.getType(), content);
).then((encryptedContent) => {
if (mRelatesTo) {
encryptedContent['m.relates_to'] = mRelatesTo;
}
event.makeEncrypted( if (mRelatesTo) {
"m.room.encrypted", encryptedContent['m.relates_to'] = mRelatesTo;
encryptedContent, }
this._olmDevice.deviceCurve25519Key,
this._olmDevice.deviceEd25519Key, event.makeEncrypted(
); "m.room.encrypted",
}); encryptedContent,
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. // at which point we'll start tracking all the users of that room.
console.log("Initial sync performed - resetting device tracking state"); console.log("Initial sync performed - resetting device tracking state");
this._deviceList.stopTrackingAllDeviceLists(); 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 // Check we really don't share any rooms with these users
// any more: the server isn't required to give us the // any more: the server isn't required to give us the
// exact correct set. // exact correct set.
const e2eUserIds = new Set(await this._getE2eUsers()); const e2eUserIds = new Set(await this._getTrackedE2eUsers());
deviceLists.left.forEach((u) => { deviceLists.left.forEach((u) => {
if (!e2eUserIds.has(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 * 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 * @returns {string[]} List of user IDs
*/ */
Crypto.prototype._getE2eUsers = async function() { Crypto.prototype._getTrackedE2eUsers = async function() {
const e2eUserIds = []; const e2eUserIds = [];
for (const room of this._getE2eRooms()) { for (const room of this._getTrackedE2eRooms()) {
const members = await room.getEncryptionTargetMembers(); const members = await room.getEncryptionTargetMembers();
for (const member of members) { for (const member of members) {
e2eUserIds.push(member.userId); 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[]} * @returns {module:models.Room[]}
*/ */
Crypto.prototype._getE2eRooms = function() { Crypto.prototype._getTrackedE2eRooms = function() {
return this._clientStore.getRooms().filter((room) => { return this._clientStore.getRooms().filter((room) => {
// check for rooms with encryption enabled // check for rooms with encryption enabled
const alg = this._roomEncryptors[room.roomId]; const alg = this._roomEncryptors[room.roomId];
if (!alg) { if (!alg) {
return false; return false;
} }
if (!this._roomDeviceTrackingState[room.roomId]) {
return false;
}
// ignore any rooms which we have left // ignore any rooms which we have left
const myMembership = room.getMyMembership(); const myMembership = room.getMyMembership();
@@ -1079,15 +1183,20 @@ Crypto.prototype._onRoomMembership = function(event, member, oldMembership) {
// not encrypting in this room // not encrypting in this room
return; return;
} }
// only mark users in this room as tracked if we already started tracking in this room
if (member.membership == 'join') { // this way we don't start device queries after sync on behalf of this room which we won't use
console.log('Join event for ' + member.userId + ' in ' + roomId); // the result of anyway, as we'll need to do a query again once all the members are fetched
// make sure we are tracking the deviceList for this user // by calling _trackRoomDevices
this._deviceList.startTrackingDeviceList(member.userId); if (this._roomDeviceTrackingState[roomId]) {
} else if (member.membership == 'invite' && if (member.membership == 'join') {
this._clientStore.getRoom(roomId).shouldEncryptForInvitedMembers()) { console.log('Join event for ' + member.userId + ' in ' + roomId);
console.log('Invite event for ' + member.userId + ' in ' + roomId); // make sure we are tracking the deviceList for this user
this._deviceList.startTrackingDeviceList(member.userId); 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); alg.onRoomMembership(event, member, oldMembership);

View File

@@ -443,6 +443,22 @@ RoomState.prototype.markOutOfBandMembersFailed = function() {
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED; 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. * Sets the loaded out-of-band members.
* @param {MatrixEvent[]} stateEvents array of membership state events * @param {MatrixEvent[]} stateEvents array of membership state events
@@ -495,7 +511,7 @@ RoomState.prototype._setOutOfBandMember = function(stateEvent) {
this._setStateEvent(stateEvent); this._setStateEvent(stateEvent);
this._updateMember(member); 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. * according to the room's state.
*/ */
RoomState.prototype._maySendEventOfType = function(eventType, userId, 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', ''); const power_levels_event = this.getStateEvents('m.room.power_levels', '');
let power_levels; let power_levels;
@@ -663,25 +674,34 @@ RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
let state_default = 0; let state_default = 0;
let events_default = 0; let events_default = 0;
let powerLevel = 0;
if (power_levels_event) { if (power_levels_event) {
power_levels = power_levels_event.getContent(); power_levels = power_levels_event.getContent();
events_levels = power_levels.events || {}; 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; state_default = power_levels.state_default;
} else { } else {
state_default = 50; 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; events_default = power_levels.events_default;
} }
} }
let required_level = state ? state_default : 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]; required_level = events_levels[eventType];
} }
return member.powerLevel >= required_level; return powerLevel >= required_level;
}; };
/** /**

View File

@@ -22,6 +22,7 @@ const EventEmitter = require("events").EventEmitter;
const EventStatus = require("./event").EventStatus; const EventStatus = require("./event").EventStatus;
const RoomSummary = require("./room-summary"); const RoomSummary = require("./room-summary");
const RoomMember = require("./room-member");
const MatrixEvent = require("./event").MatrixEvent; const MatrixEvent = require("./event").MatrixEvent;
const utils = require("../utils"); const utils = require("../utils");
const ContentRepo = require("../content-repo"); const ContentRepo = require("../content-repo");
@@ -279,14 +280,84 @@ Room.prototype.getDMInviter = function() {
} }
if (this._syncedMembership === "invite") { if (this._syncedMembership === "invite") {
// fall back to summary information // fall back to summary information
const memberCount = this.currentState.getJoinedMemberCount() + const memberCount = this.getInvitedAndJoinedMemberCount();
this.currentState.getInvitedMemberCount();
if (memberCount == 2 && this._summaryHeroes.length) { if (memberCount == 2 && this._summaryHeroes.length) {
return this._summaryHeroes[0]; 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 * Sets the membership this room was received as during sync
* @param {string} membership join | leave | invite * @param {string} membership join | leave | invite
@@ -298,7 +369,6 @@ Room.prototype.setSyncedMembership = function(membership) {
Room.prototype._loadMembersFromServer = async function() { Room.prototype._loadMembersFromServer = async function() {
const lastSyncToken = this._client.store.getSyncToken(); const lastSyncToken = this._client.store.getSyncToken();
const queryString = utils.encodeParams({ const queryString = utils.encodeParams({
membership: "join",
not_membership: "leave", not_membership: "leave",
at: lastSyncToken, at: lastSyncToken,
}); });
@@ -343,8 +413,21 @@ Room.prototype.loadMembersIfNeeded = function() {
// the OOB members // the OOB members
this.currentState.markOutOfBandMembersStarted(); this.currentState.markOutOfBandMembersStarted();
const promise = this._loadMembers().then(({memberEvents, fromServer}) => { const inMemoryUpdate = this._loadMembers().then((result) => {
this.currentState.setOutOfBandMembers(memberEvents); 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) { if (fromServer) {
const oobMembers = this.currentState.getMembers() const oobMembers = this.currentState.getMembers()
.filter((m) => m.isOutOfBand()) .filter((m) => m.isOutOfBand())
@@ -361,16 +444,40 @@ Room.prototype.loadMembersIfNeeded = function() {
}); });
} }
}).catch((err) => { }).catch((err) => {
// allow retries on fail // as this is not awaited anywhere,
this._membersPromise = null; // at least show the error in the console
this.currentState.markOutOfBandMembersFailed(); console.error(err);
throw err;
}); });
this._membersPromise = promise; this._membersPromise = inMemoryUpdate;
return this._membersPromise; 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. * Reset the live timeline of all timelineSets, and start new ones.
* *
@@ -485,8 +592,13 @@ Room.prototype.setSummary = function(summary) {
if (Number.isInteger(invitedCount)) { if (Number.isInteger(invitedCount)) {
this.currentState.setInvitedMemberCount(invitedCount); this.currentState.setInvitedMemberCount(invitedCount);
} }
if (heroes) { if (Array.isArray(heroes)) {
this._summaryHeroes = 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(); 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. * Get a list of members with given membership state.
* @param {string} membership The membership state. * @param {string} membership The membership state.
@@ -1373,6 +1493,17 @@ Room.prototype.getAccountData = function(type) {
return this.accountData[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 * This is an internal method. Calculates the name of the room from the current
* room state. * room state.

View File

@@ -71,7 +71,7 @@ function selectQuery(store, keyRange, resultMapper) {
}); });
} }
function promiseifyTxn(txn) { function txnAsPromise(txn) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
txn.oncomplete = function(event) { txn.oncomplete = function(event) {
resolve(event); resolve(event);
@@ -82,7 +82,7 @@ function promiseifyTxn(txn) {
}); });
} }
function promiseifyRequest(req) { function reqAsEventPromise(req) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
req.onsuccess = function(event) { req.onsuccess = function(event) {
resolve(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 * Does the actual reading from and writing to the indexeddb
* *
@@ -159,7 +170,7 @@ LocalIndexedDBStoreBackend.prototype = {
console.log( console.log(
`LocalIndexedDBStoreBackend.connect: awaiting connection...`, `LocalIndexedDBStoreBackend.connect: awaiting connection...`,
); );
return promiseifyRequest(req).then((ev) => { return reqAsEventPromise(req).then((ev) => {
console.log( console.log(
`LocalIndexedDBStoreBackend.connect: connected`, `LocalIndexedDBStoreBackend.connect: connected`,
); );
@@ -254,42 +265,66 @@ LocalIndexedDBStoreBackend.prototype = {
* marked as fetched, and getOutOfBandMembers will return an empty array instead of null * marked as fetched, and getOutOfBandMembers will return an empty array instead of null
* @param {string} roomId * @param {string} roomId
* @param {event[]} membershipEvents the membership events to store * @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}` + console.log(`LL: backend about to store ${membershipEvents.length}` +
` members for ${roomId}`); ` members for ${roomId}`);
function ignoreResult() {} const tx = this.db.transaction(["oob_membership_events"], "readwrite");
// run everything in a promise so anything that throws will reject const store = tx.objectStore("oob_membership_events");
return new Promise((resolve) =>{ membershipEvents.forEach((e) => {
const tx = this.db.transaction(["oob_membership_events"], "readwrite"); store.put(e);
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
// 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);
// 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}!`);
}); });
// aside from all the events, we also write a marker object to the store
// to mark the fact that OOB members have been written for this room.
// It's possible that 0 members need to be written as all where previously know
// but we still need to know whether to return null or [] from getOutOfBandMembers
// where null means out of band members haven't been stored yet for this room
const markerObject = {
room_id: roomId,
oob_written: true,
state_key: 0,
};
store.put(markerObject);
await txnAsPromise(tx);
console.log(`LL: backend done storing for ${roomId}!`);
},
clearOutOfBandMembers: async function(roomId) {
// the approach to delete all members for a room
// is to get the min and max state key from the index
// for that room, and then delete between those
// keys in the store.
// this should be way faster than deleting every member
// individually for a large room.
const readTx = this.db.transaction(
["oob_membership_events"],
"readonly");
const store = readTx.objectStore("oob_membership_events");
const roomIndex = store.index("room");
const roomRange = IDBKeyRange.only(roomId);
const minStateKeyProm = reqAsCursorPromise(
roomIndex.openKeyCursor(roomRange, "next"),
).then((cursor) => cursor && cursor.primaryKey[1]);
const maxStateKeyProm = reqAsCursorPromise(
roomIndex.openKeyCursor(roomRange, "prev"),
).then((cursor) => cursor && cursor.primaryKey[1]);
const [minStateKey, maxStateKey] = await Promise.all(
[minStateKeyProm, maxStateKeyProm]);
const writeTx = this.db.transaction(
["oob_membership_events"],
"readwrite");
const writeStore = writeTx.objectStore("oob_membership_events");
const membersKeyRange = IDBKeyRange.bound(
[roomId, minStateKey],
[roomId, maxStateKey],
);
console.log(`LL: Deleting all users + marker in storage for ` +
`room ${roomId}, with key range:`,
[roomId, minStateKey], [roomId, maxStateKey]);
await reqAsPromise(writeStore.delete(membersKeyRange));
}, },
/** /**
@@ -389,7 +424,7 @@ LocalIndexedDBStoreBackend.prototype = {
roomsData: roomsData, roomsData: roomsData,
groupsData: groupsData, groupsData: groupsData,
}); // put == UPSERT }); // put == UPSERT
return promiseifyTxn(txn); return txnAsPromise(txn);
}); });
}, },
@@ -406,7 +441,7 @@ LocalIndexedDBStoreBackend.prototype = {
for (let i = 0; i < accountData.length; i++) { for (let i = 0; i < accountData.length; i++) {
store.put(accountData[i]); // put == UPSERT store.put(accountData[i]); // put == UPSERT
} }
return promiseifyTxn(txn); return txnAsPromise(txn);
}); });
}, },
@@ -428,7 +463,7 @@ LocalIndexedDBStoreBackend.prototype = {
event: tuple[1], event: tuple[1],
}); // put == UPSERT }); // put == UPSERT
} }
return promiseifyTxn(txn); return txnAsPromise(txn);
}); });
}, },

View File

@@ -110,6 +110,10 @@ RemoteIndexedDBStoreBackend.prototype = {
return this._doCmd('setOutOfBandMembers', [roomId, membershipEvents]); 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. * Load all user presence events from the database. This is not cached.
* @return {Promise<Object[]>} A list of presence events in their raw form. * @return {Promise<Object[]>} A list of presence events in their raw form.

View File

@@ -95,6 +95,9 @@ class IndexedDBStoreWorker {
case 'getOutOfBandMembers': case 'getOutOfBandMembers':
prom = this.backend.getOutOfBandMembers(msg.args[0]); prom = this.backend.getOutOfBandMembers(msg.args[0]);
break; break;
case 'clearOutOfBandMembers':
prom = this.backend.clearOutOfBandMembers(msg.args[0]);
break;
case 'setOutOfBandMembers': case 'setOutOfBandMembers':
prom = this.backend.setOutOfBandMembers(msg.args[0], msg.args[1]); prom = this.backend.setOutOfBandMembers(msg.args[0], msg.args[1]);
break; break;

View File

@@ -242,4 +242,8 @@ IndexedDBStore.prototype.setOutOfBandMembers = function(roomId, membershipEvents
return this.backend.setOutOfBandMembers(roomId, membershipEvents); return this.backend.setOutOfBandMembers(roomId, membershipEvents);
}; };
IndexedDBStore.prototype.clearOutOfBandMembers = function(roomId) {
return this.backend.clearOutOfBandMembers(roomId);
};
module.exports.IndexedDBStore = IndexedDBStore; module.exports.IndexedDBStore = IndexedDBStore;

View File

@@ -272,6 +272,10 @@ StubStore.prototype = {
setOutOfBandMembers: function() { setOutOfBandMembers: function() {
return Promise.resolve(); return Promise.resolve();
}, },
clearOutOfBandMembers: function() {
return Promise.resolve();
},
}; };
/** Stub Store class. */ /** Stub Store class. */

View File

@@ -779,7 +779,18 @@ SyncApi.prototype._onSyncError = function(err, syncOptions) {
// erroneous. We set the state to 'reconnecting' // erroneous. We set the state to 'reconnecting'
// instead, so that clients can observe this state // instead, so that clients can observe this state
// if they wish. // 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); this._sync(syncOptions);
}); });
@@ -1145,6 +1156,8 @@ SyncApi.prototype._processSyncResponse = async function(
accountDataEvents.forEach(function(e) { accountDataEvents.forEach(function(e) {
client.emit("event", e); client.emit("event", e);
}); });
room.onLeft();
}); });
// update the notification timeline, if appropriate. // 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 * On failure, schedules a call back to itself. On success, resolves
* this._connectionReturnedDefer. * 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; const self = this;
function success() { function success() {
clearTimeout(self._keepAliveTimer); clearTimeout(self._keepAliveTimer);
if (self._connectionReturnedDefer) { if (self._connectionReturnedDefer) {
self._connectionReturnedDefer.resolve(); self._connectionReturnedDefer.resolve(connDidFail);
self._connectionReturnedDefer = null; self._connectionReturnedDefer = null;
} }
} }
@@ -1240,7 +1256,7 @@ SyncApi.prototype._pokeKeepAlive = function() {
).done(function() { ).done(function() {
success(); success();
}, function(err) { }, 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 // treat this as a success because the server probably just doesn't
// support /versions: point is, we're getting a response. // support /versions: point is, we're getting a response.
// We wait a short time though, just in case somehow the server // 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. // responses fail, this will mean we don't hammer in a loop.
self._keepAliveTimer = setTimeout(success, 2000); self._keepAliveTimer = setTimeout(success, 2000);
} else { } else {
connDidFail = true;
self._keepAliveTimer = setTimeout( self._keepAliveTimer = setTimeout(
self._pokeKeepAlive.bind(self), self._pokeKeepAlive.bind(self, connDidFail),
5000 + Math.floor(Math.random() * 5000), 5000 + Math.floor(Math.random() * 5000),
); );
// A keepalive has failed, so we emit the // A keepalive has failed, so we emit the
@@ -1257,7 +1274,7 @@ SyncApi.prototype._pokeKeepAlive = function() {
// first failure). // first failure).
// Note we do this after setting the timer: // Note we do this after setting the timer:
// this lets the unit tests advance the mock // 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 }); self._updateSyncState("ERROR", { error: err });
} }
}); });