Compare commits

...

27 Commits

Author SHA1 Message Date
David Baker 464f84d8cd v0.8.3-rc.1 2017-09-19 10:44:58 +01:00
David Baker fe0ee6402a Prepare changelog for v0.8.3-rc.1 2017-09-19 10:44:57 +01:00
David Baker 068939f790 APIs for flair (#542) 2017-09-18 14:44:40 +01:00
Matthew Hodgson 35f48d1c8e Merge pull request #526 from t3chguy/allow_trailing_slash
consume trailing slash when creating Matrix Client in HS and IS urls
2017-09-17 22:53:48 +01:00
Matthew Hodgson 52adde2501 Merge pull request #539 from turt2live/travis/ignored_users
Add ignore users API
2017-09-17 21:47:03 +01:00
David Baker 0ddc4eceaf Merge pull request #540 from matrix-org/dbkr/jsdoc_355
Upgrade to jsdoc 3.5.5
2017-09-15 09:19:21 +01:00
turt2live b0ab8c750d Add isUserIgnored convenience method
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-09-14 19:47:16 -06:00
Travis Ralston b17dd8351f Make linter happy
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-09-14 19:24:18 -06:00
Travis Ralston 0ceb8d159a Generate the ignored users event content correctly
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-09-14 16:06:55 -06:00
David Baker 402b943ddb Upgrade to jsdoc 3.5.5
Because of https://github.com/jsdoc3/jsdoc/issues/1438
2017-09-14 22:33:10 +01:00
Travis Ralston be55451c90 Add ignore users API
Signed-off-by: Travis Ralston <travpc@gmail.com>
2017-09-14 12:55:48 -06:00
Matthew Hodgson c51c1a2ae6 Merge pull request #538 from matrix-org/dbkr/reemit_use_fewer_closures
Make re-emitting events much more memory efficient
2017-09-13 12:51:26 +01:00
David Baker 845c796b96 Make re-emitting events much more memory efficient
The previous impl bluntly created a closure for every event type
and source emitter we set up a re-emit for. We can do much better
than this fairly easily by having one bound handler for each event
name and moving it into a class so we have one emitter per target,
since 99% of the time the target is the client object.
2017-09-13 11:55:03 +01:00
David Baker b0918ef293 Merge pull request #536 from matrix-org/dbkr/only_reemit_events_when_needed
Only re-emit events from Event objects if needed
2017-09-12 19:26:58 +01:00
David Baker 102572b088 Empty commit to get Travis to re-test 2017-09-12 17:37:16 +01:00
David Baker 63076e77f5 Only re-emit events from Event objects if needed
The only event and Event emits is 'Event.decrypted', so don't
bother to add listeners if the event isn't encrypted.
2017-09-12 17:09:01 +01:00
Richard van der Hoff 8e48ee5f66 Merge pull request #535 from matrix-org/rav/devicelist_bits
Handle 'left' users in the deviceList mananagement
2017-09-08 16:21:58 +01:00
Richard van der Hoff 1a55f550c0 Handle 'left' users in the deviceList mananagement
When we no longer share any rooms with a given user, the server will stop
sending us updates on their device list, and will (once synapse is updated)
send us a notification of that fact via the 'left' field in the device_lists
field in /sync, or the response from /keys/changes.
2017-09-08 15:40:26 +01:00
Richard van der Hoff ae8fc64394 Do /keys/changes before second /sync
This will avoid races between /keys/changes and /syncs.
2017-09-08 15:40:26 +01:00
David Baker 5e8e56caf9 Merge pull request #534 from matrix-org/rav/factor_out_device_tests
Factor out devicelist integration tests to a separate file
2017-09-08 13:51:11 +01:00
David Baker c075c161c2 Merge pull request #533 from matrix-org/rav/refactor_sync
Refactor sync._sync as an async function
2017-09-08 13:51:05 +01:00
Richard van der Hoff 237a553d15 sync: s/self/this/ 2017-09-08 12:07:19 +01:00
Richard van der Hoff ca8674e0de Factor out devicelist integration tests to a separate file
There's a fuzzy line between the megolm tests and the devicelist ones, but
since I want to add more tests for devicelists, we might as well put the ones
which are definitely about devicelists in their own file
2017-09-08 11:54:16 +01:00
Richard van der Hoff 0511a1172f Refactor sync._sync as an async function
This stuff is much more clearly expressed as an async function than as a
promise chain, and I'm about to add more stuff to the chain, so let's refactor
now.

There is plenty more that could be done here (self -> this, for instance), but
I've gone for keeping the diff minimal.
2017-09-08 11:53:10 +01:00
David Baker e07b304914 Merge pull request #532 from matrix-org/rav/eslint_es6
Add es6 to eslint environments
2017-09-07 10:33:52 +01:00
Richard van der Hoff 17364e72ec Add es6 to eslint environments 2017-09-07 10:29:27 +01:00
Michael Telatynski 01f93e0970 consume trailing slash when creating Matrix Client in HS and IS urls
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-08-19 11:00:46 +01:00
14 changed files with 751 additions and 390 deletions
+4
View File
@@ -9,6 +9,10 @@ module.exports = {
env: {
browser: true,
node: true,
// babel's transform-runtime converts references to ES6 globals such as
// Promise and Map to core-js polyfills, so we can use ES6 globals.
es6: true,
},
extends: ["eslint:recommended", "google"],
rules: {
+23
View File
@@ -1,3 +1,26 @@
Changes in [0.8.3-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.3-rc.1) (2017-09-19)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.2...v0.8.3-rc.1)
* consume trailing slash when creating Matrix Client in HS and IS urls
[\#526](https://github.com/matrix-org/matrix-js-sdk/pull/526)
* Add ignore users API
[\#539](https://github.com/matrix-org/matrix-js-sdk/pull/539)
* Upgrade to jsdoc 3.5.5
[\#540](https://github.com/matrix-org/matrix-js-sdk/pull/540)
* Make re-emitting events much more memory efficient
[\#538](https://github.com/matrix-org/matrix-js-sdk/pull/538)
* Only re-emit events from Event objects if needed
[\#536](https://github.com/matrix-org/matrix-js-sdk/pull/536)
* Handle 'left' users in the deviceList mananagement
[\#535](https://github.com/matrix-org/matrix-js-sdk/pull/535)
* Factor out devicelist integration tests to a separate file
[\#534](https://github.com/matrix-org/matrix-js-sdk/pull/534)
* Refactor sync._sync as an async function
[\#533](https://github.com/matrix-org/matrix-js-sdk/pull/533)
* Add es6 to eslint environments
[\#532](https://github.com/matrix-org/matrix-js-sdk/pull/532)
Changes in [0.8.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.2) (2017-08-24)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.1...v0.8.2)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "0.8.2",
"version": "0.8.3-rc.1",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
@@ -67,7 +67,7 @@
"exorcist": "^0.4.0",
"expect": "^1.20.2",
"istanbul": "^0.4.5",
"jsdoc": "^3.5.0",
"jsdoc": "^3.5.5",
"lolex": "^1.5.2",
"matrix-mock-request": "^1.2.0",
"mocha": "^3.2.0",
+10 -2
View File
@@ -33,12 +33,20 @@ import Promise from 'bluebird';
* @param {string} userId
* @param {string} deviceId
* @param {string} accessToken
*
* @param {WebStorage=} sessionStoreBackend a web storage object to use for the
* session store. If undefined, we will create a MockStorageApi.
*/
export default function TestClient(userId, deviceId, accessToken) {
export default function TestClient(
userId, deviceId, accessToken, sessionStoreBackend,
) {
this.userId = userId;
this.deviceId = deviceId;
this.storage = new sdk.WebStorageSessionStore(new testUtils.MockStorageApi());
if (sessionStoreBackend === undefined) {
sessionStoreBackend = new testUtils.MockStorageApi();
}
this.storage = new sdk.WebStorageSessionStore(sessionStoreBackend);
this.httpBackend = new MockHttpBackend();
this.client = sdk.createClient({
baseUrl: "http://" + userId + ".test.server",
+369
View File
@@ -0,0 +1,369 @@
import expect from 'expect';
import Promise from 'bluebird';
import TestClient from '../TestClient';
import testUtils from '../test-utils';
const ROOM_ID = "!room:id";
/**
* get a /sync response which contains a single e2e room (ROOM_ID), with the
* members given
*
* @param {string[]} roomMembers
*
* @return {object} sync response
*/
function getSyncResponse(roomMembers) {
const stateEvents = [
testUtils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
}),
];
Array.prototype.push.apply(
stateEvents,
roomMembers.map(
(m) => testUtils.mkMembership({
mship: 'join',
sender: m,
}),
),
);
const syncResponse = {
next_batch: 1,
rooms: {
join: {
[ROOM_ID]: {
state: {
events: stateEvents,
},
},
},
},
};
return syncResponse;
}
describe("DeviceList management:", function() {
if (!global.Olm) {
console.warn('not running deviceList tests: Olm not present');
return;
}
let sessionStoreBackend;
let aliceTestClient;
async function createTestClient() {
const testClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend,
);
await testClient.client.initCrypto();
return testClient;
}
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
// we create our own sessionStoreBackend so that we can use it for
// another TestClient.
sessionStoreBackend = new testUtils.MockStorageApi();
aliceTestClient = await createTestClient();
});
afterEach(function() {
aliceTestClient.stop();
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']);
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
console.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:xyz': {},
},
});
return Promise.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
console.log("Telling alice to send a megolm message");
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, {
event_id: '$event_id',
});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
});
});
it("We should not get confused by out-of-order device query responses",
() => {
// https://github.com/vector-im/riot-web/issues/3126
return aliceTestClient.start().then(() => {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
return aliceTestClient.flushSync();
}).then(() => {
// to make sure the initial device queries are flushed out, we
// attempt to send a message.
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {
device_keys: {
'@bob:xyz': {},
'@chris:abc': {},
},
},
);
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
200, {event_id: '$event1'});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
() => aliceTestClient.httpBackend.flush('/send/', 1),
),
]);
}).then(() => {
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(1);
// invalidate bob's and chris's device lists in separate syncs
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '2',
device_lists: {
changed: ['@bob:xyz'],
},
});
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '3',
device_lists: {
changed: ['@chris:abc'],
},
});
// flush both syncs
return aliceTestClient.flushSync().then(() => {
return aliceTestClient.flushSync();
});
}).then(() => {
// check that we don't yet have a request for chris's devices.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@chris:abc': {},
},
token: '3',
}).respond(200, {
device_keys: {'@chris:abc': {}},
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(0);
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
if (bobStat != 1 && bobStat != 2) {
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
bobStat);
}
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
chrisStat);
}
// now add an expectation for a query for bob's devices, and let
// it complete.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@bob:xyz': {},
},
token: '2',
}).respond(200, {
device_keys: {'@bob:xyz': {}},
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
}).then(() => {
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toEqual(3);
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
bobStat);
}
// now let the query for chris's devices complete.
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@chris:abc']);
}).then(() => {
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
expect(bobStat).toEqual(3);
expect(chrisStat).toEqual(3);
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(3);
});
});
// https://github.com/vector-im/riot-web/issues/4983
describe("Alice should know she has stale device lists", () => {
beforeEach(async function() {
await aliceTestClient.start();
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz']));
await aliceTestClient.flushSync();
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {
device_keys: {
'@bob:xyz': {},
},
},
);
await aliceTestClient.httpBackend.flush('/keys/query', 1);
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toBeGreaterThan(
0, "Alice should be tracking bob's device list",
);
});
it("when Bob leaves", async function() {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, {
next_batch: 2,
device_lists: {
left: ['@bob:xyz'],
},
rooms: {
join: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: 'leave',
sender: '@bob:xyz',
}),
],
},
},
},
},
},
);
await aliceTestClient.flushSync();
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
});
it("when Alice leaves", async function() {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, {
next_batch: 2,
device_lists: {
left: ['@bob:xyz'],
},
rooms: {
leave: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: 'leave',
sender: '@bob:xyz',
}),
],
},
},
},
},
},
);
await aliceTestClient.flushSync();
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
});
it("when Bob leaves whilst Alice is offline", async function() {
aliceTestClient.stop();
const anotherTestClient = await createTestClient();
try {
anotherTestClient.httpBackend.when('GET', '/keys/changes').respond(
200, {
changed: [],
left: ['@bob:xyz'],
},
);
await anotherTestClient.start();
anotherTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse([]));
await anotherTestClient.flushSync();
const bobStat = anotherTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
} finally {
anotherTestClient.stop();
}
});
});
});
-162
View File
@@ -580,46 +580,6 @@ describe("megolm", function() {
});
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']);
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
console.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:xyz': {},
},
});
return Promise.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
console.log("Telling alice to send a megolm message");
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, {
event_id: '$event_id',
});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
});
});
it("We shouldn't attempt to send to blocked devices", function() {
return aliceTestClient.start().then(() => {
// establish an olm session with alice
@@ -917,128 +877,6 @@ describe("megolm", function() {
});
it("We should not get confused by out-of-order device query responses",
() => {
// https://github.com/vector-im/riot-web/issues/3126
return aliceTestClient.start().then(() => {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
return aliceTestClient.flushSync();
}).then(() => {
// to make sure the initial device queries are flushed out, we
// attempt to send a message.
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {
device_keys: {
'@bob:xyz': {},
'@chris:abc': {},
},
},
);
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
200, {event_id: '$event1'});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
() => aliceTestClient.httpBackend.flush('/send/', 1),
),
]);
}).then(() => {
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(1);
// invalidate bob's and chris's device lists in separate syncs
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '2',
device_lists: {
changed: ['@bob:xyz'],
},
});
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '3',
device_lists: {
changed: ['@chris:abc'],
},
});
// flush both syncs
return aliceTestClient.flushSync().then(() => {
return aliceTestClient.flushSync();
});
}).then(() => {
// check that we don't yet have a request for chris's devices.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@chris:abc': {},
},
token: '3',
}).respond(200, {
device_keys: {'@chris:abc': {}},
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(0);
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
if (bobStat != 1 && bobStat != 2) {
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
bobStat);
}
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
chrisStat);
}
// now add an expectation for a query for bob's devices, and let
// it complete.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@bob:xyz': {},
},
token: '2',
}).respond(200, {
device_keys: {'@bob:xyz': {}},
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
}).then(() => {
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toEqual(3);
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
bobStat);
}
// now let the query for chris's devices complete.
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@chris:abc']);
}).then(() => {
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
expect(bobStat).toEqual(3);
expect(chrisStat).toEqual(3);
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(3);
});
});
it("Alice exports megolm keys and imports them to a new device", function() {
let messageEncrypted;
+46
View File
@@ -0,0 +1,46 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module
*/
export default class Reemitter {
constructor(target) {
this.target = target;
// We keep one bound event handler for each event name so we know
// what event is arriving
this.boundHandlers = {};
}
_handleEvent(eventName, ...args) {
this.target.emit(eventName, ...args);
}
reEmit(source, eventNames) {
for (const eventName of eventNames) {
if (this.boundHandlers[eventName] === undefined) {
this.boundHandlers[eventName] = this._handleEvent.bind(this, eventName);
}
const boundHandler = this.boundHandlers[eventName];
source.on(eventName, boundHandler);
}
}
}
+30
View File
@@ -411,6 +411,16 @@ MatrixBaseApis.prototype.getGroupSummary = function(groupId) {
return this._http.authedRequest(undefined, "GET", path);
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group profile object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getGroupProfile = function(groupId) {
const path = utils.encodeUri("/groups/$groupId/profile", {$groupId: groupId});
return this._http.authedRequest(undefined, "GET", path);
};
/**
* @param {string} groupId
* @param {Object} profile The group profile object
@@ -515,6 +525,26 @@ MatrixBaseApis.prototype.createGroup = function(content) {
);
};
/**
* @param {string[]} userIds List of user IDs
* @return {module:client.Promise} Resolves: Object as exmaple below
*
* {
* "users": {
* "@bob:example.com": {
* "+example:example.com"
* }
* }
* }
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getPublicisedGroups = function(userIds) {
const path = utils.encodeUri("/publicised_groups");
return this._http.authedRequest(
undefined, "POST", path, undefined, { user_ids: userIds },
);
};
/**
* Retrieve a state event.
* @param {string} roomId
+49 -5
View File
@@ -40,7 +40,7 @@ const SyncApi = require("./sync");
const MatrixBaseApis = require("./base-apis");
const MatrixError = httpApi.MatrixError;
import reEmit from './reemit';
import ReEmitter from './ReEmitter';
const SCROLLBACK_DELAY_MS = 3000;
let CRYPTO_ENABLED = false;
@@ -113,8 +113,20 @@ try {
* crypto store implementation.
*/
function MatrixClient(opts) {
// Allow trailing slash in HS url
if (opts.baseUrl && opts.baseUrl.endsWith("/")) {
opts.baseUrl = opts.baseUrl.substr(0, opts.baseUrl.length - 1);
}
// Allow trailing slash in IS url
if (opts.idBaseUrl && opts.idBaseUrl.endsWith("/")) {
opts.idBaseUrl = opts.idBaseUrl.substr(0, opts.idBaseUrl.length - 1);
}
MatrixBaseApis.call(this, opts);
this.reEmitter = new ReEmitter(this);
this.store = opts.store || new StubStore();
this.deviceId = opts.deviceId || null;
@@ -364,7 +376,7 @@ MatrixClient.prototype.initCrypto = async function() {
this._cryptoStore,
);
reEmit(this, crypto, [
this.reEmitter.reEmit(crypto, [
"crypto.roomKeyRequest",
"crypto.roomKeyRequestCancellation",
]);
@@ -755,6 +767,38 @@ MatrixClient.prototype.getAccountData = function(eventType) {
return this.store.getAccountData(eventType);
};
/**
* Gets the users that are ignored by this client
* @returns {string[]} The array of users that are ignored (empty if none)
*/
MatrixClient.prototype.getIgnoredUsers = function() {
const event = this.getAccountData("m.ignored_user_list");
if (!event || !event.getContent() || !event.getContent()["ignored_users"]) return [];
return Object.keys(event.getContent()["ignored_users"]);
};
/**
* Sets the users that the current user should ignore.
* @param {string[]} userIds the user IDs to ignore
* @param {module:client.callback} [callback] Optional.
* @return {module:client.Promise} Resolves: Account data event
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixClient.prototype.setIgnoredUsers = function(userIds, callback) {
const content = {ignored_users: {}};
userIds.map((u) => content.ignored_users[u] = {});
return this.setAccountData("m.ignored_user_list", content, callback);
};
/**
* Gets whether or not a specific user is being ignored by this client.
* @param {string} userId the user ID to check
* @returns {boolean} true if the user is ignored, false otherwise
*/
MatrixClient.prototype.isUserIgnored = function(userId) {
return this.getIgnoredUsers().indexOf(userId) !== -1;
};
// Room operations
// ===============
@@ -3274,10 +3318,10 @@ function _resolve(callback, defer, res) {
function _PojoToMatrixEventMapper(client) {
function mapper(plainOldJsObject) {
const event = new MatrixEvent(plainOldJsObject);
reEmit(client, event, [
"Event.decrypted",
]);
if (event.isEncrypted()) {
client.reEmitter.reEmit(event, [
"Event.decrypted",
]);
event.attemptDecryption(client._crypto);
}
return event;
+43 -4
View File
@@ -26,8 +26,30 @@ import Promise from 'bluebird';
import DeviceInfo from './deviceinfo';
import olmlib from './olmlib';
/* State transition diagram for DeviceList._deviceTrackingStatus
*
* |
* stopTrackingDeviceList V
* +---------------------> NOT_TRACKED
* | |
* +<--------------------+ | startTrackingDeviceList
* | | V
* | +-------------> PENDING_DOWNLOAD <--------------------+-+
* | | ^ | | |
* | | restart download | | start download | | invalidateUserDeviceList
* | | client failed | | | |
* | | | V | |
* | +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
* | | | |
* +<-------------------+ | download successful |
* ^ V |
* +----------------------- UP_TO_DATE ------------------------+
*/
// constants for DeviceList._deviceTrackingStatus
// const TRACKING_STATUS_NOT_TRACKED = 0;
const TRACKING_STATUS_NOT_TRACKED = 0;
const TRACKING_STATUS_PENDING_DOWNLOAD = 1;
const TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2;
const TRACKING_STATUS_UP_TO_DATE = 3;
@@ -236,6 +258,26 @@ export default class DeviceList {
// refreshOutdatedDeviceLists.
}
/**
* Mark the given user as no longer being tracked for device-list updates.
*
* This won't affect any in-progress downloads, which will still go on to
* complete; it will just mean that we don't think that we have an up-to-date
* list for future calls to downloadKeys.
*
* @param {String} userId
*/
stopTrackingDeviceList(userId) {
if (this._deviceTrackingStatus[userId]) {
console.log('No longer tracking device list for ' + userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED;
}
// we don't yet persist the tracking status, since there may be a lot
// of calls; instead we wait for the forthcoming
// refreshOutdatedDeviceLists.
}
/**
* Mark the cached device list for the given user outdated.
*
@@ -283,9 +325,6 @@ export default class DeviceList {
usersToDownload.push(userId);
}
}
if (usersToDownload.length == 0) {
return;
}
// we didn't persist the tracking status during
// invalidateUserDeviceList, so do it now.
+37 -46
View File
@@ -69,7 +69,6 @@ function Crypto(baseApis, sessionStore, userId, deviceId,
this._olmDevice = new OlmDevice(sessionStore);
this._deviceList = new DeviceList(baseApis, sessionStore, this._olmDevice);
this._initialDeviceListInvalidationPending = false;
// the last time we did a check for the number of one-time-keys on the
// server.
@@ -150,15 +149,6 @@ Crypto.prototype.init = async function() {
*/
Crypto.prototype.registerEventHandlers = function(eventEmitter) {
const crypto = this;
eventEmitter.on("sync", function(syncState, oldState, data) {
try {
if (syncState === "SYNCING") {
crypto._onSyncCompleted(data);
}
} catch (e) {
console.error("Error handling sync", e);
}
});
eventEmitter.on("RoomMember.membership", function(event, member, oldMembership) {
try {
@@ -248,7 +238,7 @@ Crypto.prototype.uploadDeviceKeys = function() {
/**
* Stores the current one_time_key count which will be handled later (in a call of
* _onSyncCompleted). The count is e.g. coming from a /sync response.
* onSyncCompleted). The count is e.g. coming from a /sync response.
*
* @param {Number} currentCount The current count of one_time_keys to be stored
*/
@@ -785,12 +775,24 @@ Crypto.prototype.decryptEvent = function(event) {
};
/**
* Handle the notification from /sync that a user has updated their device list.
* Handle the notification from /sync or /keys/changes that device lists have
* been changed.
*
* @param {String} userId
* @param {Object} deviceLists device_lists field from /sync, or response from
* /keys/changes
*/
Crypto.prototype.userDeviceListChanged = function(userId) {
this._deviceList.invalidateUserDeviceList(userId);
Crypto.prototype.handleDeviceListChanges = async function(deviceLists) {
if (deviceLists.changed && Array.isArray(deviceLists.changed)) {
deviceLists.changed.forEach((u) => {
this._deviceList.invalidateUserDeviceList(u);
});
}
if (deviceLists.left && Array.isArray(deviceLists.left)) {
deviceLists.left.forEach((u) => {
this._deviceList.stopTrackingDeviceList(u);
});
}
// don't flush the outdated device list yet - we do it once we finish
// processing the sync.
@@ -837,7 +839,7 @@ Crypto.prototype.onCryptoEvent = async function(event) {
try {
// inhibit the device list refresh for now - it will happen once we've
// finished processing the sync, in _onSyncCompleted.
// finished processing the sync, in onSyncCompleted.
await this.setRoomEncryption(roomId, content, true);
} catch (e) {
console.error("Error configuring encryption in room " + roomId +
@@ -853,7 +855,7 @@ Crypto.prototype.onCryptoEvent = async function(event) {
*
* @param {Object} syncData the data from the 'MatrixClient.sync' event
*/
Crypto.prototype._onSyncCompleted = function(syncData) {
Crypto.prototype.onSyncCompleted = async function(syncData) {
const nextSyncToken = syncData.nextSyncToken;
if (!syncData.oldSyncToken) {
@@ -863,18 +865,15 @@ Crypto.prototype._onSyncCompleted = function(syncData) {
// invalidate devices which have changed since then.
const oldSyncToken = this._sessionStore.getEndToEndDeviceSyncToken();
if (oldSyncToken !== null) {
this._initialDeviceListInvalidationPending = true;
this._invalidateDeviceListsSince(
oldSyncToken, nextSyncToken,
).catch((e) => {
try {
await this._invalidateDeviceListsSince(
oldSyncToken, nextSyncToken,
);
} catch (e) {
// if that failed, we fall back to invalidating everyone.
console.warn("Error fetching changed device list", e);
this._deviceList.invalidateAllDeviceLists();
}).done(() => {
this._initialDeviceListInvalidationPending = false;
this._deviceList.lastKnownSyncToken = nextSyncToken;
this._deviceList.refreshOutdatedDeviceLists();
});
}
} else {
// otherwise, we have to invalidate all devices for all users we
// are tracking.
@@ -884,14 +883,12 @@ Crypto.prototype._onSyncCompleted = function(syncData) {
}
}
if (!this._initialDeviceListInvalidationPending) {
// we can now store our sync token so that we can get an update on
// restart rather than having to invalidate everyone.
//
// (we don't really need to do this on every sync - we could just
// do it periodically)
this._sessionStore.storeEndToEndDeviceSyncToken(nextSyncToken);
}
// we can now store our sync token so that we can get an update on
// restart rather than having to invalidate everyone.
//
// (we don't really need to do this on every sync - we could just
// do it periodically)
this._sessionStore.storeEndToEndDeviceSyncToken(nextSyncToken);
// catch up on any new devices we got told about during the sync.
this._deviceList.lastKnownSyncToken = nextSyncToken;
@@ -914,25 +911,19 @@ Crypto.prototype._onSyncCompleted = function(syncData) {
* @param {String} oldSyncToken
* @param {String} lastKnownSyncToken
*
* @returns {Promise} resolves once the query is complete. Rejects if the
* Returns a Promise which resolves once the query is complete. Rejects if the
* keyChange query fails.
*/
Crypto.prototype._invalidateDeviceListsSince = function(
Crypto.prototype._invalidateDeviceListsSince = async function(
oldSyncToken, lastKnownSyncToken,
) {
return this._baseApis.getKeyChanges(
const r = await this._baseApis.getKeyChanges(
oldSyncToken, lastKnownSyncToken,
).then((r) => {
console.log("got key changes since", oldSyncToken, ":", r.changed);
);
if (!r.changed || !Array.isArray(r.changed)) {
return;
}
console.log("got key changes since", oldSyncToken, ":", r);
r.changed.forEach((u) => {
this._deviceList.invalidateUserDeviceList(u);
});
});
await this.handleDeviceListChanges(r);
};
/**
+5 -3
View File
@@ -27,7 +27,7 @@ const ContentRepo = require("../content-repo");
const EventTimeline = require("./event-timeline");
const EventTimelineSet = require("./event-timeline-set");
import reEmit from '../reemit';
import ReEmitter from '../ReEmitter';
function synthesizeReceipt(userId, event, receiptType) {
// console.log("synthesizing receipt for "+event.getId());
@@ -106,6 +106,8 @@ function Room(roomId, opts) {
opts = opts || {};
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
this.reEmitter = new ReEmitter(this);
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
throw new Error(
"opts.pendingEventOrdering MUST be either 'chronological' or " +
@@ -153,7 +155,7 @@ function Room(roomId, opts) {
// all our per-room timeline sets. the first one is the unfiltered ones;
// the subsequent ones are the filtered ones in no particular order.
this._timelineSets = [new EventTimelineSet(this, opts)];
reEmit(this, this.getUnfilteredTimelineSet(),
this.reEmitter.reEmit(this.getUnfilteredTimelineSet(),
["Room.timeline", "Room.timelineReset"]);
this._fixUpLegacyTimelineFields();
@@ -490,7 +492,7 @@ Room.prototype.getOrCreateFilteredTimelineSet = function(filter) {
}
const opts = Object.assign({ filter: filter }, this._opts);
const timelineSet = new EventTimelineSet(this, opts);
reEmit(this, timelineSet, ["Room.timeline", "Room.timelineReset"]);
this.reEmitter.reEmit(timelineSet, ["Room.timeline", "Room.timelineReset"]);
this._filteredTimelineSets[filter.filterId] = timelineSet;
this._timelineSets.push(timelineSet);
-44
View File
@@ -1,44 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module
*/
/**
* re-emit events raised by one EventEmitter from another
*
* @param {external:EventEmitter} reEmitEntity
* entity from which we want events to be emitted
* @param {external:EventEmitter} emittableEntity
* entity from which events are currently emitted
* @param {Array<string>} eventNames
* list of events to be reemitted
*/
export default function reEmit(reEmitEntity, emittableEntity, eventNames) {
for (const eventName of eventNames) {
// setup a listener on the entity (the Room, User, etc) for this event
emittableEntity.on(eventName, function(...args) {
// take the args from the listener and reuse them, adding the
// event name to the arg list so it works with .emit()
// Transformation Example:
// listener on "foo" => function(a,b) { ... }
// Re-emit on "thing" => thing.emit("foo", a, b)
reEmitEntity.emit(eventName, ...args);
});
}
}
+133 -122
View File
@@ -32,8 +32,6 @@ const utils = require("./utils");
const Filter = require("./filter");
const EventTimeline = require("./models/event-timeline");
import reEmit from './reemit';
const DEBUG = true;
// /sync requests allow you to set a timeout= but the request may continue
@@ -100,7 +98,7 @@ function SyncApi(client, opts) {
this._failedSyncCount = 0; // Number of consecutive failed /sync requests
if (client.getNotifTimelineSet()) {
reEmit(client, client.getNotifTimelineSet(),
client.reEmitter.reEmit(client.getNotifTimelineSet(),
["Room.timeline", "Room.timelineReset"]);
}
}
@@ -115,7 +113,7 @@ SyncApi.prototype.createRoom = function(roomId) {
pendingEventOrdering: this.opts.pendingEventOrdering,
timelineSupport: client.timelineSupport,
});
reEmit(client, room, ["Room.name", "Room.timeline", "Room.redaction",
client.reEmitter.reEmit(room, ["Room.name", "Room.timeline", "Room.redaction",
"Room.receipt", "Room.tags",
"Room.timelineReset",
"Room.localEchoUpdated",
@@ -132,7 +130,7 @@ SyncApi.prototype.createRoom = function(roomId) {
SyncApi.prototype.createGroup = function(groupId) {
const client = this.client;
const group = new Group(groupId);
reEmit(client, group, ["Group.profile", "Group.myMembership"]);
client.reEmitter.reEmit(group, ["Group.profile", "Group.myMembership"]);
return group;
};
@@ -145,13 +143,13 @@ SyncApi.prototype._registerStateListeners = function(room) {
// we need to also re-emit room state and room member events, so hook it up
// to the client now. We need to add a listener for RoomState.members in
// order to hook them correctly. (TODO: find a better way?)
reEmit(client, room.currentState, [
client.reEmitter.reEmit(room.currentState, [
"RoomState.events", "RoomState.members", "RoomState.newMember",
]);
room.currentState.on("RoomState.newMember", function(event, state, member) {
member.user = client.getUser(member.userId);
reEmit(
client, member,
client.reEmitter.reEmit(
member,
[
"RoomMember.name", "RoomMember.typing", "RoomMember.powerLevel",
"RoomMember.membership",
@@ -498,15 +496,14 @@ SyncApi.prototype.retryImmediately = function() {
* @param {string} syncOptions.filterId
* @param {boolean} syncOptions.hasSyncedBefore
*/
SyncApi.prototype._sync = function(syncOptions) {
SyncApi.prototype._sync = async function(syncOptions) {
const client = this.client;
const self = this;
if (!this._running) {
debuglog("Sync no longer running: exiting.");
if (self._connectionReturnedDefer) {
self._connectionReturnedDefer.reject();
self._connectionReturnedDefer = null;
if (this._connectionReturnedDefer) {
this._connectionReturnedDefer.reject();
this._connectionReturnedDefer = null;
}
this._updateSyncState("STOPPED");
return;
@@ -562,124 +559,134 @@ SyncApi.prototype._sync = function(syncOptions) {
qps.timeout = 0;
}
let isCachedResponse = false;
let syncPromise;
let savedSync;
if (!syncOptions.hasSyncedBefore) {
// Don't do an HTTP hit to /sync. Instead, load up the persisted /sync data,
// if there is data there.
syncPromise = client.store.getSavedSync();
} else {
syncPromise = Promise.resolve(null);
savedSync = await client.store.getSavedSync();
}
syncPromise.then((savedSync) => {
if (savedSync) {
debuglog("sync(): not doing HTTP hit, instead returning stored /sync data");
isCachedResponse = true;
return {
next_batch: savedSync.nextBatch,
rooms: savedSync.roomsData,
groups: savedSync.groupsData,
account_data: {
events: savedSync.accountData,
},
};
} else {
let isCachedResponse = false;
let data;
if (savedSync) {
debuglog("sync(): not doing HTTP hit, instead returning stored /sync data");
isCachedResponse = true;
data = {
next_batch: savedSync.nextBatch,
rooms: savedSync.roomsData,
groups: savedSync.groupsData,
account_data: {
events: savedSync.accountData,
},
};
} else {
try {
//debuglog('Starting sync since=' + syncToken);
this._currentSyncRequest = client._http.authedRequest(
undefined, "GET", "/sync", qps, undefined, clientSideTimeoutMs,
);
return this._currentSyncRequest;
}
}).then(function(data) {
//debuglog('Completed sync, next_batch=' + data.next_batch);
// set the sync token NOW *before* processing the events. We do this so
// if something barfs on an event we can skip it rather than constantly
// polling with the same token.
client.store.setSyncToken(data.next_batch);
// Reset after a successful sync
self._failedSyncCount = 0;
// We need to wait until the sync data has been sent to the backend
// because it appears that the sync data gets modified somewhere in
// processing it in such a way as to make it no longer cloneable.
// XXX: Find out what is modifying it!
if (!isCachedResponse) {
// Don't give the store back its own cached data
return client.store.setSyncData(data).then(() => {
return data;
});
} else {
return Promise.resolve(data);
}
}).done((data) => {
self._processSyncResponse(syncToken, data).catch((e) => {
// log the exception with stack if we have it, else fall back
// to the plain description
console.error("Caught /sync error", e.stack || e);
}).then(() => {
// emit synced events
const syncEventData = {
oldSyncToken: syncToken,
nextSyncToken: data.next_batch,
catchingUp: self._catchingUp,
};
if (!syncOptions.hasSyncedBefore) {
self._updateSyncState("PREPARED", syncEventData);
syncOptions.hasSyncedBefore = true;
}
// keep emitting SYNCING -> SYNCING for clients who want to do bulk updates
if (!isCachedResponse) {
self._updateSyncState("SYNCING", syncEventData);
// tell databases that everything is now in a consistent state and can be
// saved (no point doing so if we only have the data we just got out of the
// store).
client.store.save();
}
// Begin next sync
self._sync(syncOptions);
});
}, function(err) {
if (!self._running) {
debuglog("Sync no longer running: exiting");
if (self._connectionReturnedDefer) {
self._connectionReturnedDefer.reject();
self._connectionReturnedDefer = null;
}
self._updateSyncState("STOPPED");
data = await this._currentSyncRequest;
} catch (e) {
this._onSyncError(e, syncOptions);
return;
}
console.error("/sync error %s", err);
console.error(err);
}
self._failedSyncCount++;
console.log('Number of consecutive failed sync requests:', self._failedSyncCount);
//debuglog('Completed sync, next_batch=' + data.next_batch);
debuglog("Starting keep-alive");
// Note that we do *not* mark the sync connection as
// lost yet: we only do this if a keepalive poke
// fails, since long lived HTTP connections will
// go away sometimes and we shouldn't treat this as
// erroneous. We set the state to 'reconnecting'
// instead, so that clients can onserve this state
// if they wish.
self._startKeepAlives().done(function() {
self._sync(syncOptions);
});
self._currentSyncRequest = null;
// Transition from RECONNECTING to ERROR after a given number of failed syncs
self._updateSyncState(
self._failedSyncCount >= FAILED_SYNC_ERROR_THRESHOLD ?
"ERROR" : "RECONNECTING",
);
// set the sync token NOW *before* processing the events. We do this so
// if something barfs on an event we can skip it rather than constantly
// polling with the same token.
client.store.setSyncToken(data.next_batch);
// Reset after a successful sync
this._failedSyncCount = 0;
// We need to wait until the sync data has been sent to the backend
// because it appears that the sync data gets modified somewhere in
// processing it in such a way as to make it no longer cloneable.
// XXX: Find out what is modifying it!
if (!isCachedResponse) {
// Don't give the store back its own cached data
await client.store.setSyncData(data);
}
try {
await this._processSyncResponse(syncToken, data);
} catch(e) {
// log the exception with stack if we have it, else fall back
// to the plain description
console.error("Caught /sync error", e.stack || e);
}
// emit synced events
const syncEventData = {
oldSyncToken: syncToken,
nextSyncToken: data.next_batch,
catchingUp: this._catchingUp,
};
if (!syncOptions.hasSyncedBefore) {
this._updateSyncState("PREPARED", syncEventData);
syncOptions.hasSyncedBefore = true;
}
if (!isCachedResponse) {
// tell the crypto module to do its processing. It may block (to do a
// /keys/changes request).
if (this.opts.crypto) {
await this.opts.crypto.onSyncCompleted(syncEventData);
}
// keep emitting SYNCING -> SYNCING for clients who want to do bulk updates
this._updateSyncState("SYNCING", syncEventData);
// tell databases that everything is now in a consistent state and can be
// saved (no point doing so if we only have the data we just got out of the
// store).
client.store.save();
}
// Begin next sync
this._sync(syncOptions);
};
SyncApi.prototype._onSyncError = function(err, syncOptions) {
if (!this._running) {
debuglog("Sync no longer running: exiting");
if (this._connectionReturnedDefer) {
this._connectionReturnedDefer.reject();
this._connectionReturnedDefer = null;
}
this._updateSyncState("STOPPED");
return;
}
console.error("/sync error %s", err);
console.error(err);
this._failedSyncCount++;
console.log('Number of consecutive failed sync requests:', this._failedSyncCount);
debuglog("Starting keep-alive");
// Note that we do *not* mark the sync connection as
// lost yet: we only do this if a keepalive poke
// fails, since long lived HTTP connections will
// go away sometimes and we shouldn't treat this as
// erroneous. We set the state to 'reconnecting'
// instead, so that clients can onserve this state
// if they wish.
this._startKeepAlives().then(() => {
this._sync(syncOptions);
});
this._currentSyncRequest = null;
// Transition from RECONNECTING to ERROR after a given number of failed syncs
this._updateSyncState(
this._failedSyncCount >= FAILED_SYNC_ERROR_THRESHOLD ?
"ERROR" : "RECONNECTING",
);
};
/**
@@ -1015,10 +1022,14 @@ SyncApi.prototype._processSyncResponse = async function(syncToken, data) {
}
// Handle device list updates
if (this.opts.crypto && data.device_lists && data.device_lists.changed) {
data.device_lists.changed.forEach((u) => {
this.opts.crypto.userDeviceListChanged(u);
});
if (data.device_lists) {
if (this.opts.crypto) {
await this.opts.crypto.handleDeviceListChanges(data.device_lists);
} else {
// FIXME if we *don't* have a crypto module, we still need to
// invalidate the device lists. But that would require a
// substantial bit of rework :/.
}
}
// Handle one_time_keys_count
@@ -1324,7 +1335,7 @@ SyncApi.prototype._onOnline = function() {
function createNewUser(client, userId) {
const user = new User(userId);
reEmit(client, user, [
client.reEmitter.reEmit(user, [
"User.avatarUrl", "User.displayName", "User.presence",
"User.currentlyActive", "User.lastPresenceTs",
]);