Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 464f84d8cd | |||
| fe0ee6402a | |||
| 068939f790 | |||
| 35f48d1c8e | |||
| 52adde2501 | |||
| 0ddc4eceaf | |||
| b0ab8c750d | |||
| b17dd8351f | |||
| 0ceb8d159a | |||
| 402b943ddb | |||
| be55451c90 | |||
| c51c1a2ae6 | |||
| 845c796b96 | |||
| b0918ef293 | |||
| 102572b088 | |||
| 63076e77f5 | |||
| 8e48ee5f66 | |||
| 1a55f550c0 | |||
| ae8fc64394 | |||
| 5e8e56caf9 | |||
| c075c161c2 | |||
| 237a553d15 | |||
| ca8674e0de | |||
| 0511a1172f | |||
| e07b304914 | |||
| 17364e72ec | |||
| 22b213ae26 | |||
| 7d5936a9e9 | |||
| ab8f466f53 | |||
| 201177e7f0 | |||
| ec5f9a2892 | |||
| ee5b8748b5 | |||
| 8d04f8b8b5 | |||
| 033babfbfc | |||
| 15b77861ea | |||
| c4721850ce | |||
| b325aad5c9 | |||
| 92e616f18e | |||
| f7fee29c76 | |||
| eccea7411f | |||
| 01f93e0970 | |||
| 2d82a7bc2e | |||
| ca91fba071 | |||
| 9f2fce4d87 | |||
| e1942267c5 | |||
| 12212409c7 | |||
| e5565c6bdb | |||
| f00558d840 | |||
| da0dc5ed11 | |||
| e8be38ce5a | |||
| 38c9a05a0c | |||
| 110bd332f4 | |||
| 8a0f73bf81 | |||
| 337c9cbea3 | |||
| cfd61096d9 | |||
| 2894e253a2 | |||
| e52985e082 | |||
| 7d2bc12bb7 | |||
| a5f397b26d | |||
| 5b93d5210e | |||
| e943a6e09c | |||
| 8f527a6212 | |||
| f2f8ad6b65 | |||
| c870930bc0 | |||
| b26b1caa86 | |||
| 6613ee6b0d | |||
| 9550bca099 |
@@ -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: {
|
||||
|
||||
@@ -1,3 +1,52 @@
|
||||
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)
|
||||
|
||||
* Handle m.call.* events which are decrypted asynchronously
|
||||
[\#530](https://github.com/matrix-org/matrix-js-sdk/pull/530)
|
||||
* Re-emit events from, er, Event objects
|
||||
[\#529](https://github.com/matrix-org/matrix-js-sdk/pull/529)
|
||||
|
||||
Changes in [0.8.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.1) (2017-08-23)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.1-rc.1...v0.8.1)
|
||||
|
||||
* [No changes]
|
||||
|
||||
Changes in [0.8.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.1-rc.1) (2017-08-22)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.8.0...v0.8.1-rc.1)
|
||||
|
||||
* Fix error handling in interactive-auth
|
||||
[\#527](https://github.com/matrix-org/matrix-js-sdk/pull/527)
|
||||
* Make lots of OlmDevice asynchronous
|
||||
[\#524](https://github.com/matrix-org/matrix-js-sdk/pull/524)
|
||||
* Make crypto.decryptMessage return decryption results
|
||||
[\#523](https://github.com/matrix-org/matrix-js-sdk/pull/523)
|
||||
|
||||
Changes in [0.8.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.8.0) (2017-08-15)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.7.13...v0.8.0)
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "0.8.0",
|
||||
"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
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -133,7 +133,7 @@ function expectAliClaimKeys() {
|
||||
// it can take a while to process the key query, so give it some extra
|
||||
// time, and make sure the claim actually happens rather than ploughing on
|
||||
// confusingly.
|
||||
return aliTestClient.httpBackend.flush("/keys/claim", 1).then((r) => {
|
||||
return aliTestClient.httpBackend.flush("/keys/claim", 1, 500).then((r) => {
|
||||
expect(r).toEqual(1, "Ali did not claim Bob's keys");
|
||||
});
|
||||
});
|
||||
@@ -314,31 +314,37 @@ function recvMessage(httpBackend, client, sender, message) {
|
||||
},
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
const deferred = Promise.defer();
|
||||
const onEvent = function(event) {
|
||||
console.log(client.credentials.userId + " received event",
|
||||
event);
|
||||
|
||||
// ignore the m.room.member events
|
||||
if (event.getType() == "m.room.member") {
|
||||
return;
|
||||
}
|
||||
const eventPromise = new Promise((resolve, reject) => {
|
||||
const onEvent = function(event) {
|
||||
// ignore the m.room.member events
|
||||
if (event.getType() == "m.room.member") {
|
||||
return;
|
||||
}
|
||||
console.log(client.credentials.userId + " received event",
|
||||
event);
|
||||
|
||||
client.removeListener("event", onEvent);
|
||||
resolve(event);
|
||||
};
|
||||
client.on("event", onEvent);
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
|
||||
return eventPromise.then((event) => {
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
|
||||
// it may still be being decrypted
|
||||
return testUtils.awaitDecryption(event);
|
||||
}).then((event) => {
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent()).toEqual({
|
||||
msgtype: "m.text",
|
||||
body: "Hello, World",
|
||||
});
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
|
||||
client.removeListener("event", onEvent);
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
client.on("event", onEvent);
|
||||
|
||||
httpBackend.flush();
|
||||
return deferred.promise;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -583,27 +589,25 @@ describe("MatrixClient crypto", function() {
|
||||
};
|
||||
bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
const deferred = Promise.defer();
|
||||
const onEvent = function(event) {
|
||||
console.log(bobUserId + " received event",
|
||||
event);
|
||||
|
||||
// ignore the m.room.member events
|
||||
if (event.getType() == "m.room.member") {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
bobTestClient.client.once("event", onEvent);
|
||||
const eventPromise = new Promise((resolve, reject) => {
|
||||
const onEvent = function(event) {
|
||||
console.log(bobUserId + " received event",
|
||||
event);
|
||||
resolve(event);
|
||||
};
|
||||
bobTestClient.client.once("event", onEvent);
|
||||
});
|
||||
|
||||
bobTestClient.httpBackend.flush();
|
||||
return deferred.promise;
|
||||
return eventPromise;
|
||||
}).then((event) => {
|
||||
expect(event.isEncrypted()).toBeTruthy();
|
||||
|
||||
// it may still be being decrypted
|
||||
return testUtils.awaitDecryption(event);
|
||||
}).then((event) => {
|
||||
expect(event.getType()).toEqual("m.room.message");
|
||||
expect(event.getContent().msgtype).toEqual("m.bad.encrypted");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+16
-162
@@ -119,6 +119,7 @@ function encryptMegolmEvent(opts) {
|
||||
}
|
||||
|
||||
return {
|
||||
event_id: 'test_megolm_event',
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: opts.groupSession.encrypt(JSON.stringify(plaintext)),
|
||||
@@ -342,6 +343,9 @@ describe("megolm", function() {
|
||||
}).then(function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.isEncrypted()).toBe(true);
|
||||
return testUtils.awaitDecryption(event);
|
||||
}).then((event) => {
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
});
|
||||
});
|
||||
@@ -405,6 +409,18 @@ describe("megolm", function() {
|
||||
}).then(function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
|
||||
if (event.getContent().msgtype != 'm.bad.encrypted') {
|
||||
return event;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
event.once('Event.decrypted', (ev) => {
|
||||
console.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
}).then((event) => {
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
});
|
||||
});
|
||||
@@ -564,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
|
||||
@@ -901,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;
|
||||
|
||||
|
||||
@@ -219,3 +219,25 @@ module.exports.MockStorageApi.prototype = {
|
||||
delete this.data[k];
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* If an event is being decrypted, wait for it to finish being decrypted.
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
|
||||
*/
|
||||
module.exports.awaitDecryption = function(event) {
|
||||
if (!event.isBeingDecrypted()) {
|
||||
return Promise.resolve(event);
|
||||
}
|
||||
|
||||
console.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
event.once('Event.decrypted', (ev) => {
|
||||
console.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -71,9 +71,13 @@ describe("MegolmDecryption", function() {
|
||||
groupSession = new global.Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
|
||||
const event = new MatrixEvent({});
|
||||
event.setClearData(
|
||||
{
|
||||
// construct a fake decrypted key event via the use of a mocked
|
||||
// 'crypto' implementation.
|
||||
const event = new MatrixEvent({
|
||||
type: 'm.room.encrypted',
|
||||
});
|
||||
const decryptedData = {
|
||||
clearEvent: {
|
||||
type: 'm.room_key',
|
||||
content: {
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
@@ -82,11 +86,19 @@ describe("MegolmDecryption", function() {
|
||||
session_key: groupSession.session_key(),
|
||||
},
|
||||
},
|
||||
"SENDER_CURVE25519",
|
||||
"SENDER_ED25519",
|
||||
);
|
||||
senderCurve25519Key: "SENDER_CURVE25519",
|
||||
claimedEd25519Key: "SENDER_ED25519",
|
||||
};
|
||||
|
||||
megolmDecryption.onRoomKeyEvent(event);
|
||||
const mockCrypto = {
|
||||
decryptEvent: function() {
|
||||
return Promise.resolve(decryptedData);
|
||||
},
|
||||
};
|
||||
|
||||
return event.attemptDecryption(mockCrypto).then(() => {
|
||||
megolmDecryption.onRoomKeyEvent(event);
|
||||
});
|
||||
});
|
||||
|
||||
it('can decrypt an event', function() {
|
||||
@@ -104,8 +116,8 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
});
|
||||
|
||||
return megolmDecryption.decryptEvent(event).then(() => {
|
||||
expect(event.getContent()).toEqual('testytest');
|
||||
return megolmDecryption.decryptEvent(event).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,12 +141,16 @@ describe("MegolmDecryption", function() {
|
||||
const deviceInfo = {};
|
||||
mockCrypto.getStoredDevice.andReturn(deviceInfo);
|
||||
|
||||
const awaitEnsureSessions = new Promise((res, rej) => {
|
||||
mockOlmLib.ensureOlmSessionsForDevices.andCall(() => {
|
||||
mockOlmLib.ensureOlmSessionsForDevices.andReturn(
|
||||
Promise.resolve({'@alice:foo': {'alidevice': {
|
||||
sessionId: 'alisession',
|
||||
}}}),
|
||||
);
|
||||
|
||||
const awaitEncryptForDevice = new Promise((res, rej) => {
|
||||
mockOlmLib.encryptMessageForDevice.andCall(() => {
|
||||
res();
|
||||
return Promise.resolve({'@alice:foo': {'alidevice': {
|
||||
sessionId: 'alisession',
|
||||
}}});
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -144,7 +160,7 @@ describe("MegolmDecryption", function() {
|
||||
megolmDecryption.shareKeysWithDevice(keyRequest);
|
||||
|
||||
// it's asynchronous, so we have to wait a bit
|
||||
return awaitEnsureSessions;
|
||||
return awaitEncryptForDevice;
|
||||
}).then(() => {
|
||||
// check that it called encryptMessageForDevice with
|
||||
// appropriate args.
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
import sdk from '../..';
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
|
||||
import testUtils from '../test-utils';
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
|
||||
});
|
||||
|
||||
describe(".attemptDecryption", () => {
|
||||
let encryptedEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
encryptedEvent = new MatrixEvent({
|
||||
id: 'test_encrypted_event',
|
||||
type: 'm.room.encrypted',
|
||||
content: {
|
||||
ciphertext: 'secrets',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should retry decryption if a retry is queued', () => {
|
||||
let callCount = 0;
|
||||
|
||||
let prom2;
|
||||
|
||||
const crypto = {
|
||||
decryptEvent: function() {
|
||||
++callCount;
|
||||
console.log(`decrypt: ${callCount}`);
|
||||
if (callCount == 1) {
|
||||
// schedule a second decryption attempt while
|
||||
// the first one is still running.
|
||||
prom2 = encryptedEvent.attemptDecryption(crypto);
|
||||
|
||||
const error = new Error("nope");
|
||||
error.name = 'DecryptionError';
|
||||
return Promise.reject(error);
|
||||
} else {
|
||||
expect(prom2.isFulfilled()).toBe(
|
||||
false, 'second attemptDecryption resolved too soon');
|
||||
|
||||
return Promise.resolve({
|
||||
clearEvent: {
|
||||
type: 'm.room.message',
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return encryptedEvent.attemptDecryption(crypto).then(() => {
|
||||
expect(callCount).toEqual(2);
|
||||
expect(encryptedEvent.getType()).toEqual('m.room.message');
|
||||
|
||||
// make sure the second attemptDecryption resolves
|
||||
return prom2;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -438,6 +448,60 @@ MatrixBaseApis.prototype.getGroupUsers = function(groupId) {
|
||||
return this._http.authedRequest(undefined, "GET", path);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @param {string} userId
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.inviteUserToGroup = function(groupId, userId) {
|
||||
const path = utils.encodeUri(
|
||||
"/groups/$groupId/admin/users/invite/$userId",
|
||||
{$groupId: groupId, $userId: userId},
|
||||
);
|
||||
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @param {string} userId
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.removeUserFromGroup = function(groupId, userId) {
|
||||
const path = utils.encodeUri(
|
||||
"/groups/$groupId/admin/users/remove/$userId",
|
||||
{$groupId: groupId, $userId: userId},
|
||||
);
|
||||
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.acceptGroupInvite = function(groupId) {
|
||||
const path = utils.encodeUri(
|
||||
"/groups/$groupId/self/accept_invite",
|
||||
{$groupId: groupId},
|
||||
);
|
||||
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @return {module:client.Promise} Resolves: Empty object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.leaveGroup = function(groupId) {
|
||||
const path = utils.encodeUri(
|
||||
"/groups/$groupId/self/leave",
|
||||
{$groupId: groupId},
|
||||
);
|
||||
return this._http.authedRequest(undefined, "PUT", path, undefined, {});
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {module:client.Promise} Resolves: The groups to which the user is joined
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
@@ -461,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
|
||||
|
||||
+101
-10
@@ -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",
|
||||
]);
|
||||
@@ -659,6 +671,30 @@ MatrixClient.prototype.importRoomKeys = function(keys) {
|
||||
return this._crypto.importRoomKeys(keys);
|
||||
};
|
||||
|
||||
// Group ops
|
||||
// =========
|
||||
// Operations on groups that come down the sync stream (ie. ones the
|
||||
// user is a member of or invited to)
|
||||
|
||||
/**
|
||||
* Get the group for the given group ID.
|
||||
* This function will return a valid group for any group for which a Group event
|
||||
* has been emitted.
|
||||
* @param {string} groupId The group ID
|
||||
* @return {Group} The Group or null if the group is not known or there is no data store.
|
||||
*/
|
||||
MatrixClient.prototype.getGroup = function(groupId) {
|
||||
return this.store.getGroup(groupId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve all known groups.
|
||||
* @return {Groups[]} A list of groups, or an empty list if there is no data store.
|
||||
*/
|
||||
MatrixClient.prototype.getGroups = function() {
|
||||
return this.store.getGroups();
|
||||
};
|
||||
|
||||
// Room ops
|
||||
// ========
|
||||
|
||||
@@ -731,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
|
||||
// ===============
|
||||
|
||||
@@ -3029,6 +3097,10 @@ function setupCallEventHandler(client) {
|
||||
// now loop through the buffer chronologically and inject them
|
||||
callEventBuffer.forEach(function(e) {
|
||||
if (ignoreCallIds[e.getContent().call_id]) {
|
||||
console.log(
|
||||
'Ignoring previously answered/hungup call ' +
|
||||
e.getContent().call_id,
|
||||
);
|
||||
return;
|
||||
}
|
||||
callEventHandler(e);
|
||||
@@ -3037,20 +3109,25 @@ function setupCallEventHandler(client) {
|
||||
}
|
||||
});
|
||||
|
||||
client.on("event", function(event) {
|
||||
if (!isClientPrepared) {
|
||||
if (event.getType().indexOf("m.call.") === 0) {
|
||||
callEventBuffer.push(event);
|
||||
client.on("event", onEvent);
|
||||
|
||||
function onEvent(event) {
|
||||
if (event.getType().indexOf("m.call.") !== 0) {
|
||||
// not a call event
|
||||
if (event.isBeingDecrypted() || event.isDecryptionFailure()) {
|
||||
// not *yet* a call event, but might become one...
|
||||
event.once("Event.decrypted", onEvent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!isClientPrepared) {
|
||||
callEventBuffer.push(event);
|
||||
return;
|
||||
}
|
||||
callEventHandler(event);
|
||||
});
|
||||
}
|
||||
|
||||
function callEventHandler(event) {
|
||||
if (event.getType().indexOf("m.call.") !== 0) {
|
||||
return; // not a call event
|
||||
}
|
||||
const content = event.getContent();
|
||||
let call = content.call_id ? client.callList[content.call_id] : undefined;
|
||||
let i;
|
||||
@@ -3242,6 +3319,9 @@ function _PojoToMatrixEventMapper(client) {
|
||||
function mapper(plainOldJsObject) {
|
||||
const event = new MatrixEvent(plainOldJsObject);
|
||||
if (event.isEncrypted()) {
|
||||
client.reEmitter.reEmit(event, [
|
||||
"Event.decrypted",
|
||||
]);
|
||||
event.attemptDecryption(client._crypto);
|
||||
}
|
||||
return event;
|
||||
@@ -3423,6 +3503,17 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever the sdk learns about a new group. <strong>This event
|
||||
* is experimental and may change.</strong>
|
||||
* @event module:client~MatrixClient#"Group"
|
||||
* @param {Group} group The newly created, fully populated group.
|
||||
* @example
|
||||
* matrixClient.on("Group", function(group){
|
||||
* var groupId = group.groupId;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever a new Room is added. This will fire when you are invited to a
|
||||
* room, as well as when you join a room. <strong>This event is experimental and
|
||||
|
||||
@@ -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.
|
||||
|
||||
+65
-46
@@ -83,17 +83,10 @@ function OlmDevice(sessionStore) {
|
||||
this._sessionStore = sessionStore;
|
||||
this._pickleKey = "DEFAULT_KEY";
|
||||
|
||||
let e2eKeys;
|
||||
const account = new Olm.Account();
|
||||
try {
|
||||
_initialise_account(this._sessionStore, this._pickleKey, account);
|
||||
e2eKeys = JSON.parse(account.identity_keys());
|
||||
} finally {
|
||||
account.free();
|
||||
}
|
||||
|
||||
this.deviceCurve25519Key = e2eKeys.curve25519;
|
||||
this.deviceEd25519Key = e2eKeys.ed25519;
|
||||
// don't know these until we load the account from storage in init()
|
||||
this.deviceCurve25519Key = null;
|
||||
this.deviceEd25519Key = null;
|
||||
this._maxOneTimeKeys = null;
|
||||
|
||||
// we don't bother stashing outboundgroupsessions in the sessionstore -
|
||||
// instead we keep them here.
|
||||
@@ -112,6 +105,32 @@ function OlmDevice(sessionStore) {
|
||||
this._inboundGroupSessionMessageIndexes = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the OlmAccount. This must be called before any other operations
|
||||
* on the OlmDevice.
|
||||
*
|
||||
* Attempts to load the OlmAccount from localStorage, or creates one if none is
|
||||
* found.
|
||||
*
|
||||
* Reads the device keys from the OlmAccount object.
|
||||
*/
|
||||
OlmDevice.prototype.init = async function() {
|
||||
let e2eKeys;
|
||||
const account = new Olm.Account();
|
||||
try {
|
||||
_initialise_account(this._sessionStore, this._pickleKey, account);
|
||||
e2eKeys = JSON.parse(account.identity_keys());
|
||||
|
||||
this._maxOneTimeKeys = account.max_number_of_one_time_keys();
|
||||
} finally {
|
||||
account.free();
|
||||
}
|
||||
|
||||
this.deviceCurve25519Key = e2eKeys.curve25519;
|
||||
this.deviceEd25519Key = e2eKeys.ed25519;
|
||||
};
|
||||
|
||||
|
||||
function _initialise_account(sessionStore, pickleKey, account) {
|
||||
const e2eAccount = sessionStore.getEndToEndAccount();
|
||||
if (e2eAccount !== null) {
|
||||
@@ -222,9 +241,9 @@ OlmDevice.prototype._getUtility = function(func) {
|
||||
* Signs a message with the ed25519 key for this account.
|
||||
*
|
||||
* @param {string} message message to be signed
|
||||
* @return {string} base64-encoded signature
|
||||
* @return {Promise<string>} base64-encoded signature
|
||||
*/
|
||||
OlmDevice.prototype.sign = function(message) {
|
||||
OlmDevice.prototype.sign = async function(message) {
|
||||
return this._getAccount(function(account) {
|
||||
return account.sign(message);
|
||||
});
|
||||
@@ -237,7 +256,7 @@ OlmDevice.prototype.sign = function(message) {
|
||||
* <tt>curve25519</tt>, which is itself an object mapping key id to Curve25519
|
||||
* key.
|
||||
*/
|
||||
OlmDevice.prototype.getOneTimeKeys = function() {
|
||||
OlmDevice.prototype.getOneTimeKeys = async function() {
|
||||
return this._getAccount(function(account) {
|
||||
return JSON.parse(account.one_time_keys());
|
||||
});
|
||||
@@ -250,15 +269,13 @@ OlmDevice.prototype.getOneTimeKeys = function() {
|
||||
* @return {number} number of keys
|
||||
*/
|
||||
OlmDevice.prototype.maxNumberOfOneTimeKeys = function() {
|
||||
return this._getAccount(function(account) {
|
||||
return account.max_number_of_one_time_keys();
|
||||
});
|
||||
return this._maxOneTimeKeys;
|
||||
};
|
||||
|
||||
/**
|
||||
* Marks all of the one-time keys as published.
|
||||
*/
|
||||
OlmDevice.prototype.markKeysAsPublished = function() {
|
||||
OlmDevice.prototype.markKeysAsPublished = async function() {
|
||||
const self = this;
|
||||
this._getAccount(function(account) {
|
||||
account.mark_keys_as_published();
|
||||
@@ -271,7 +288,7 @@ OlmDevice.prototype.markKeysAsPublished = function() {
|
||||
*
|
||||
* @param {number} numKeys number of keys to generate
|
||||
*/
|
||||
OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
|
||||
OlmDevice.prototype.generateOneTimeKeys = async function(numKeys) {
|
||||
const self = this;
|
||||
this._getAccount(function(account) {
|
||||
account.generate_one_time_keys(numKeys);
|
||||
@@ -288,7 +305,7 @@ OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
|
||||
* @param {string} theirOneTimeKey remote user's one-time Curve25519 key
|
||||
* @return {string} sessionId for the outbound session.
|
||||
*/
|
||||
OlmDevice.prototype.createOutboundSession = function(
|
||||
OlmDevice.prototype.createOutboundSession = async function(
|
||||
theirIdentityKey, theirOneTimeKey,
|
||||
) {
|
||||
const self = this;
|
||||
@@ -318,7 +335,7 @@ OlmDevice.prototype.createOutboundSession = function(
|
||||
* @raises {Error} if the received message was not valid (for instance, it
|
||||
* didn't use a valid one-time key).
|
||||
*/
|
||||
OlmDevice.prototype.createInboundSession = function(
|
||||
OlmDevice.prototype.createInboundSession = async function(
|
||||
theirDeviceIdentityKey, message_type, ciphertext,
|
||||
) {
|
||||
if (message_type !== 0) {
|
||||
@@ -353,9 +370,9 @@ OlmDevice.prototype.createInboundSession = function(
|
||||
*
|
||||
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
|
||||
* remote device
|
||||
* @return {string[]} a list of known session ids for the device
|
||||
* @return {Promise<string[]>} a list of known session ids for the device
|
||||
*/
|
||||
OlmDevice.prototype.getSessionIdsForDevice = function(theirDeviceIdentityKey) {
|
||||
OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityKey) {
|
||||
const sessions = this._sessionStore.getEndToEndSessions(
|
||||
theirDeviceIdentityKey,
|
||||
);
|
||||
@@ -367,10 +384,10 @@ OlmDevice.prototype.getSessionIdsForDevice = function(theirDeviceIdentityKey) {
|
||||
*
|
||||
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
|
||||
* remote device
|
||||
* @return {string?} session id, or null if no established session
|
||||
* @return {Promise<?string>} session id, or null if no established session
|
||||
*/
|
||||
OlmDevice.prototype.getSessionIdForDevice = function(theirDeviceIdentityKey) {
|
||||
const sessionIds = this.getSessionIdsForDevice(theirDeviceIdentityKey);
|
||||
OlmDevice.prototype.getSessionIdForDevice = async function(theirDeviceIdentityKey) {
|
||||
const sessionIds = await this.getSessionIdsForDevice(theirDeviceIdentityKey);
|
||||
if (sessionIds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -390,8 +407,8 @@ OlmDevice.prototype.getSessionIdForDevice = function(theirDeviceIdentityKey) {
|
||||
* @param {string} deviceIdentityKey Curve25519 identity key for the device
|
||||
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
|
||||
*/
|
||||
OlmDevice.prototype.getSessionInfoForDevice = function(deviceIdentityKey) {
|
||||
const sessionIds = this.getSessionIdsForDevice(deviceIdentityKey);
|
||||
OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey) {
|
||||
const sessionIds = await this.getSessionIdsForDevice(deviceIdentityKey);
|
||||
sessionIds.sort();
|
||||
|
||||
const info = [];
|
||||
@@ -419,9 +436,9 @@ OlmDevice.prototype.getSessionInfoForDevice = function(deviceIdentityKey) {
|
||||
* @param {string} sessionId the id of the active session
|
||||
* @param {string} payloadString payload to be encrypted and sent
|
||||
*
|
||||
* @return {string} ciphertext
|
||||
* @return {Promise<string>} ciphertext
|
||||
*/
|
||||
OlmDevice.prototype.encryptMessage = function(
|
||||
OlmDevice.prototype.encryptMessage = async function(
|
||||
theirDeviceIdentityKey, sessionId, payloadString,
|
||||
) {
|
||||
const self = this;
|
||||
@@ -444,9 +461,9 @@ OlmDevice.prototype.encryptMessage = function(
|
||||
* @param {number} message_type message_type field from the received message
|
||||
* @param {string} ciphertext base64-encoded body from the received message
|
||||
*
|
||||
* @return {string} decrypted payload.
|
||||
* @return {Promise<string>} decrypted payload.
|
||||
*/
|
||||
OlmDevice.prototype.decryptMessage = function(
|
||||
OlmDevice.prototype.decryptMessage = async function(
|
||||
theirDeviceIdentityKey, sessionId, message_type, ciphertext,
|
||||
) {
|
||||
const self = this;
|
||||
@@ -468,10 +485,10 @@ OlmDevice.prototype.decryptMessage = function(
|
||||
* @param {number} message_type message_type field from the received message
|
||||
* @param {string} ciphertext base64-encoded body from the received message
|
||||
*
|
||||
* @return {boolean} true if the received message is a prekey message which matches
|
||||
* @return {Promise<boolean>} true if the received message is a prekey message which matches
|
||||
* the given session.
|
||||
*/
|
||||
OlmDevice.prototype.matchesSession = function(
|
||||
OlmDevice.prototype.matchesSession = async function(
|
||||
theirDeviceIdentityKey, sessionId, message_type, ciphertext,
|
||||
) {
|
||||
if (message_type !== 0) {
|
||||
@@ -669,7 +686,7 @@ OlmDevice.prototype._getInboundGroupSession = function(
|
||||
* @param {boolean} exportFormat true if the megolm keys are in export format
|
||||
* (ie, they lack an ed25519 signature)
|
||||
*/
|
||||
OlmDevice.prototype.addInboundGroupSession = function(
|
||||
OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
roomId, senderKey, forwardingCurve25519KeyChain,
|
||||
sessionId, sessionKey, keysClaimed,
|
||||
exportFormat,
|
||||
@@ -727,7 +744,7 @@ OlmDevice.prototype.addInboundGroupSession = function(
|
||||
*
|
||||
* @param {module:crypto/OlmDevice.MegolmSessionData} data session data
|
||||
*/
|
||||
OlmDevice.prototype.importInboundGroupSession = function(data) {
|
||||
OlmDevice.prototype.importInboundGroupSession = async function(data) {
|
||||
/* if we already have this session, consider updating it */
|
||||
function updateSession(session, sessionData) {
|
||||
console.log("Update for megolm session " + data.sender_key + "|" +
|
||||
@@ -780,11 +797,11 @@ OlmDevice.prototype.importInboundGroupSession = function(data) {
|
||||
*
|
||||
* @return {null} the sessionId is unknown
|
||||
*
|
||||
* @return {{result: string, senderKey: string,
|
||||
* @return {Promise<{result: string, senderKey: string,
|
||||
* forwardingCurve25519KeyChain: Array<string>,
|
||||
* keysClaimed: Object<string, string>}}
|
||||
* keysClaimed: Object<string, string>}>}
|
||||
*/
|
||||
OlmDevice.prototype.decryptGroupMessage = function(
|
||||
OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
roomId, senderKey, sessionId, body,
|
||||
) {
|
||||
const self = this;
|
||||
@@ -832,9 +849,9 @@ OlmDevice.prototype.decryptGroupMessage = function(
|
||||
* @param {string} senderKey base64-encoded curve25519 key of the sender
|
||||
* @param {sring} sessionId session identifier
|
||||
*
|
||||
* @returns {boolean} true if we have the keys to this session
|
||||
* @returns {Promise<boolean>} true if we have the keys to this session
|
||||
*/
|
||||
OlmDevice.prototype.hasInboundSessionKeys = function(roomId, senderKey, sessionId) {
|
||||
OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, sessionId) {
|
||||
const s = this._sessionStore.getEndToEndInboundGroupSession(
|
||||
senderKey, sessionId,
|
||||
);
|
||||
@@ -863,14 +880,16 @@ OlmDevice.prototype.hasInboundSessionKeys = function(roomId, senderKey, sessionI
|
||||
* @param {string} senderKey base64-encoded curve25519 key of the sender
|
||||
* @param {string} sessionId session identifier
|
||||
*
|
||||
* @returns {{chain_index: number, key: string,
|
||||
* @returns {Promise<{chain_index: number, key: string,
|
||||
* forwarding_curve25519_key_chain: Array<string>,
|
||||
* sender_claimed_ed25519_key: string
|
||||
* }}
|
||||
* }>}
|
||||
* details of the session key. The key is a base64-encoded megolm key in
|
||||
* export format.
|
||||
*/
|
||||
OlmDevice.prototype.getInboundGroupSessionKey = function(roomId, senderKey, sessionId) {
|
||||
OlmDevice.prototype.getInboundGroupSessionKey = async function(
|
||||
roomId, senderKey, sessionId,
|
||||
) {
|
||||
function getKey(session, sessionData) {
|
||||
const messageIndex = session.first_known_index();
|
||||
|
||||
@@ -896,9 +915,9 @@ OlmDevice.prototype.getInboundGroupSessionKey = function(roomId, senderKey, sess
|
||||
*
|
||||
* @param {string} senderKey base64-encoded curve25519 key of the sender
|
||||
* @param {string} sessionId session identifier
|
||||
* @return {module:crypto/OlmDevice.MegolmSessionData} exported session data
|
||||
* @return {Promise<module:crypto/OlmDevice.MegolmSessionData>} exported session data
|
||||
*/
|
||||
OlmDevice.prototype.exportInboundGroupSession = function(senderKey, sessionId) {
|
||||
OlmDevice.prototype.exportInboundGroupSession = async function(senderKey, sessionId) {
|
||||
const s = this._sessionStore.getEndToEndInboundGroupSession(
|
||||
senderKey, sessionId,
|
||||
);
|
||||
|
||||
@@ -117,7 +117,8 @@ class DecryptionAlgorithm {
|
||||
*
|
||||
* @param {MatrixEvent} event undecrypted event
|
||||
*
|
||||
* @return {Promise} resolves once we have finished decrypting. Rejects with an
|
||||
* @return {Promise<module:crypto~EventDecryptionResult>} promise which
|
||||
* resolves once we have finished decrypting. Rejects with an
|
||||
* `algorithms.DecryptionError` if there is a problem decrypting the event.
|
||||
*/
|
||||
|
||||
|
||||
@@ -249,7 +249,7 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
|
||||
const sessionId = this._olmDevice.createOutboundGroupSession();
|
||||
const key = this._olmDevice.getOutboundGroupSessionKey(sessionId);
|
||||
|
||||
this._olmDevice.addInboundGroupSession(
|
||||
await this._olmDevice.addInboundGroupSession(
|
||||
this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId,
|
||||
key.key, {ed25519: this._olmDevice.deviceEd25519Key},
|
||||
);
|
||||
@@ -535,18 +535,12 @@ utils.inherits(MegolmDecryption, base.DecryptionAlgorithm);
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
*
|
||||
* @return {Promise} resolves once we have finished decrypting. Rejects with an
|
||||
* `algorithms.DecryptionError` if there is a problem decrypting the event.
|
||||
* returns a promise which resolves to a
|
||||
* {@link module:crypto~EventDecryptionResult} once we have finished
|
||||
* decrypting, or rejects with an `algorithms.DecryptionError` if there is a
|
||||
* problem decrypting the event.
|
||||
*/
|
||||
MegolmDecryption.prototype.decryptEvent = function(event) {
|
||||
return this._decryptEvent(event, true);
|
||||
};
|
||||
|
||||
|
||||
// helper for the real decryptEvent and for _retryDecryption. If
|
||||
// requestKeysOnFail is true, we'll send an m.room_key_request when we fail
|
||||
// to decrypt the event due to missing megolm keys.
|
||||
MegolmDecryption.prototype._decryptEvent = async function(event, requestKeysOnFail) {
|
||||
MegolmDecryption.prototype.decryptEvent = async function(event) {
|
||||
const content = event.getWireContent();
|
||||
|
||||
if (!content.sender_key || !content.session_id ||
|
||||
@@ -557,15 +551,13 @@ MegolmDecryption.prototype._decryptEvent = async function(event, requestKeysOnFa
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = this._olmDevice.decryptGroupMessage(
|
||||
res = await this._olmDevice.decryptGroupMessage(
|
||||
event.getRoomId(), content.sender_key, content.session_id, content.ciphertext,
|
||||
);
|
||||
} catch (e) {
|
||||
if (e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') {
|
||||
this._addEventToPendingList(event);
|
||||
if (requestKeysOnFail) {
|
||||
this._requestKeysForEvent(event);
|
||||
}
|
||||
this._requestKeysForEvent(event);
|
||||
}
|
||||
throw new base.DecryptionError(
|
||||
e.toString(), {
|
||||
@@ -577,9 +569,7 @@ MegolmDecryption.prototype._decryptEvent = async function(event, requestKeysOnFa
|
||||
if (res === null) {
|
||||
// We've got a message for a session we don't have.
|
||||
this._addEventToPendingList(event);
|
||||
if (requestKeysOnFail) {
|
||||
this._requestKeysForEvent(event);
|
||||
}
|
||||
this._requestKeysForEvent(event);
|
||||
throw new base.DecryptionError(
|
||||
"The sender's device has not sent us the keys for this message.",
|
||||
{
|
||||
@@ -599,8 +589,12 @@ MegolmDecryption.prototype._decryptEvent = async function(event, requestKeysOnFa
|
||||
);
|
||||
}
|
||||
|
||||
event.setClearData(payload, res.senderKey, res.keysClaimed.ed25519,
|
||||
res.forwardingCurve25519KeyChain);
|
||||
return {
|
||||
clearEvent: payload,
|
||||
senderCurve25519Key: res.senderKey,
|
||||
claimedEd25519Key: res.keysClaimed.ed25519,
|
||||
forwardingCurve25519KeyChain: res.forwardingCurve25519KeyChain,
|
||||
};
|
||||
};
|
||||
|
||||
MegolmDecryption.prototype._requestKeysForEvent = function(event) {
|
||||
@@ -706,24 +700,26 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
content.room_id, senderKey, forwardingKeyChain, sessionId,
|
||||
content.session_key, keysClaimed,
|
||||
exportFormat,
|
||||
);
|
||||
).then(() => {
|
||||
// cancel any outstanding room key requests for this session
|
||||
this._crypto.cancelRoomKeyRequest({
|
||||
algorithm: content.algorithm,
|
||||
room_id: content.room_id,
|
||||
session_id: content.session_id,
|
||||
sender_key: senderKey,
|
||||
});
|
||||
|
||||
// cancel any outstanding room key requests for this session
|
||||
this._crypto.cancelRoomKeyRequest({
|
||||
algorithm: content.algorithm,
|
||||
room_id: content.room_id,
|
||||
session_id: content.session_id,
|
||||
sender_key: senderKey,
|
||||
// have another go at decrypting events sent with this session.
|
||||
this._retryDecryption(senderKey, sessionId);
|
||||
}).catch((e) => {
|
||||
console.error(`Error handling m.room_key_event: ${e}`);
|
||||
});
|
||||
|
||||
// have another go at decrypting events sent with this session.
|
||||
this._retryDecryption(senderKey, sessionId);
|
||||
};
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
MegolmDecryption.prototype.hasKeysForKeyRequest = async function(keyRequest) {
|
||||
MegolmDecryption.prototype.hasKeysForKeyRequest = function(keyRequest) {
|
||||
const body = keyRequest.requestBody;
|
||||
|
||||
return this._olmDevice.hasInboundSessionKeys(
|
||||
@@ -764,10 +760,10 @@ MegolmDecryption.prototype.shareKeysWithDevice = function(keyRequest) {
|
||||
+ userId + ":" + deviceId,
|
||||
);
|
||||
|
||||
const payload = this._buildKeyForwardingMessage(
|
||||
return this._buildKeyForwardingMessage(
|
||||
body.room_id, body.sender_key, body.session_id,
|
||||
);
|
||||
|
||||
}).then((payload) => {
|
||||
const encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: this._olmDevice.deviceCurve25519Key,
|
||||
@@ -795,10 +791,10 @@ MegolmDecryption.prototype.shareKeysWithDevice = function(keyRequest) {
|
||||
}).done();
|
||||
};
|
||||
|
||||
MegolmDecryption.prototype._buildKeyForwardingMessage = function(
|
||||
MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
|
||||
roomId, senderKey, sessionId,
|
||||
) {
|
||||
const key = this._olmDevice.getInboundGroupSessionKey(
|
||||
const key = await this._olmDevice.getInboundGroupSessionKey(
|
||||
roomId, senderKey, sessionId,
|
||||
);
|
||||
|
||||
|
||||
@@ -157,8 +157,10 @@ utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
*
|
||||
* returns a promise which resolves once we have finished decrypting. Rejects with an
|
||||
* `algorithms.DecryptionError` if there is a problem decrypting the event.
|
||||
* returns a promise which resolves to a
|
||||
* {@link module:crypto~EventDecryptionResult} once we have finished
|
||||
* decrypting. Rejects with an `algorithms.DecryptionError` if there is a
|
||||
* problem decrypting the event.
|
||||
*/
|
||||
OlmDecryption.prototype.decryptEvent = async function(event) {
|
||||
const content = event.getWireContent();
|
||||
@@ -227,9 +229,13 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
|
||||
}
|
||||
|
||||
const claimedKeys = payload.keys || {};
|
||||
event.setClearData(payload, deviceKey, claimedKeys.ed25519 || null);
|
||||
};
|
||||
|
||||
return {
|
||||
clearEvent: payload,
|
||||
senderCurve25519Key: deviceKey,
|
||||
claimedEd25519Key: claimedKeys.ed25519 || null,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempt to decrypt an Olm message
|
||||
@@ -242,14 +248,16 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
|
||||
OlmDecryption.prototype._decryptMessage = async function(
|
||||
theirDeviceIdentityKey, message,
|
||||
) {
|
||||
const sessionIds = this._olmDevice.getSessionIdsForDevice(theirDeviceIdentityKey);
|
||||
const sessionIds = await this._olmDevice.getSessionIdsForDevice(
|
||||
theirDeviceIdentityKey,
|
||||
);
|
||||
|
||||
// try each session in turn.
|
||||
const decryptionErrors = {};
|
||||
for (let i = 0; i < sessionIds.length; i++) {
|
||||
const sessionId = sessionIds[i];
|
||||
try {
|
||||
const payload = this._olmDevice.decryptMessage(
|
||||
const payload = await this._olmDevice.decryptMessage(
|
||||
theirDeviceIdentityKey, sessionId, message.type, message.body,
|
||||
);
|
||||
console.log(
|
||||
@@ -258,7 +266,7 @@ OlmDecryption.prototype._decryptMessage = async function(
|
||||
);
|
||||
return payload;
|
||||
} catch (e) {
|
||||
const foundSession = this._olmDevice.matchesSession(
|
||||
const foundSession = await this._olmDevice.matchesSession(
|
||||
theirDeviceIdentityKey, sessionId, message.type, message.body,
|
||||
);
|
||||
|
||||
@@ -296,7 +304,7 @@ OlmDecryption.prototype._decryptMessage = async function(
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = this._olmDevice.createInboundSession(
|
||||
res = await this._olmDevice.createInboundSession(
|
||||
theirDeviceIdentityKey, message.type, message.body,
|
||||
);
|
||||
} catch (e) {
|
||||
|
||||
+95
-79
@@ -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.
|
||||
@@ -88,12 +87,6 @@ function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
|
||||
this._deviceKeys = {};
|
||||
|
||||
// build our device keys: these will later be uploaded
|
||||
this._deviceKeys["ed25519:" + this._deviceId] =
|
||||
this._olmDevice.deviceEd25519Key;
|
||||
this._deviceKeys["curve25519:" + this._deviceId] =
|
||||
this._olmDevice.deviceCurve25519Key;
|
||||
|
||||
this._globalBlacklistUnverifiedDevices = false;
|
||||
|
||||
this._outgoingRoomKeyRequestManager = new OutgoingRoomKeyRequestManager(
|
||||
@@ -106,6 +99,22 @@ function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
this._receivedRoomKeyRequestCancellations = [];
|
||||
// true if we are currently processing received room key requests
|
||||
this._processingRoomKeyRequests = false;
|
||||
}
|
||||
utils.inherits(Crypto, EventEmitter);
|
||||
|
||||
/**
|
||||
* Initialise the crypto module so that it is ready for use
|
||||
*
|
||||
* Returns a promise which resolves once the crypto module is ready for use.
|
||||
*/
|
||||
Crypto.prototype.init = async function() {
|
||||
await this._olmDevice.init();
|
||||
|
||||
// build our device keys: these will later be uploaded
|
||||
this._deviceKeys["ed25519:" + this._deviceId] =
|
||||
this._olmDevice.deviceEd25519Key;
|
||||
this._deviceKeys["curve25519:" + this._deviceId] =
|
||||
this._olmDevice.deviceCurve25519Key;
|
||||
|
||||
let myDevices = this._sessionStore.getEndToEndDevicesForUser(
|
||||
this._userId,
|
||||
@@ -129,13 +138,6 @@ function Crypto(baseApis, sessionStore, userId, deviceId,
|
||||
this._userId, myDevices,
|
||||
);
|
||||
}
|
||||
}
|
||||
utils.inherits(Crypto, EventEmitter);
|
||||
|
||||
/**
|
||||
* Initialise the crypto module so that it is ready for use
|
||||
*/
|
||||
Crypto.prototype.init = async function() {
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -147,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 {
|
||||
@@ -245,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
|
||||
*/
|
||||
@@ -307,14 +300,15 @@ function _maybeUploadOneTimeKeys(crypto) {
|
||||
function uploadLoop(keyCount) {
|
||||
if (keyLimit <= keyCount) {
|
||||
// If we don't need to generate any more keys then we are done.
|
||||
return;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const keysThisLoop = Math.min(keyLimit - keyCount, maxKeysPerCycle);
|
||||
|
||||
// Ask olm to generate new one time keys, then upload them to synapse.
|
||||
crypto._olmDevice.generateOneTimeKeys(keysThisLoop);
|
||||
return _uploadOneTimeKeys(crypto).then((res) => {
|
||||
return crypto._olmDevice.generateOneTimeKeys(keysThisLoop).then(() => {
|
||||
return _uploadOneTimeKeys(crypto);
|
||||
}).then((res) => {
|
||||
if (res.one_time_key_counts && res.one_time_key_counts.signed_curve25519) {
|
||||
// if the response contains a more up to date value use this
|
||||
// for the next loop
|
||||
@@ -357,7 +351,7 @@ function _maybeUploadOneTimeKeys(crypto) {
|
||||
|
||||
// returns a promise which resolves to the response
|
||||
async function _uploadOneTimeKeys(crypto) {
|
||||
const oneTimeKeys = crypto._olmDevice.getOneTimeKeys();
|
||||
const oneTimeKeys = await crypto._olmDevice.getOneTimeKeys();
|
||||
const oneTimeJson = {};
|
||||
|
||||
const promises = [];
|
||||
@@ -382,7 +376,7 @@ async function _uploadOneTimeKeys(crypto) {
|
||||
device_id: crypto._deviceId,
|
||||
});
|
||||
|
||||
crypto._olmDevice.markKeysAsPublished();
|
||||
await crypto._olmDevice.markKeysAsPublished();
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -498,7 +492,7 @@ Crypto.prototype.getOlmSessionsForUser = async function(userId) {
|
||||
for (let j = 0; j < devices.length; ++j) {
|
||||
const device = devices[j];
|
||||
const deviceKey = device.getIdentityKey();
|
||||
const sessions = this._olmDevice.getSessionInfoForDevice(deviceKey);
|
||||
const sessions = await this._olmDevice.getSessionInfoForDevice(deviceKey);
|
||||
|
||||
result[device.deviceId] = {
|
||||
deviceIdKey: deviceKey,
|
||||
@@ -692,17 +686,16 @@ Crypto.prototype.isRoomEncrypted = function(roomId) {
|
||||
* session export objects
|
||||
*/
|
||||
Crypto.prototype.exportRoomKeys = function() {
|
||||
return Promise.resolve(
|
||||
this._sessionStore.getAllEndToEndInboundGroupSessionKeys().map(
|
||||
(s) => {
|
||||
const sess = this._olmDevice.exportInboundGroupSession(
|
||||
s.senderKey, s.sessionId,
|
||||
);
|
||||
|
||||
return Promise.map(
|
||||
this._sessionStore.getAllEndToEndInboundGroupSessionKeys(),
|
||||
(s) => {
|
||||
return this._olmDevice.exportInboundGroupSession(
|
||||
s.senderKey, s.sessionId,
|
||||
).then((sess) => {
|
||||
sess.algorithm = olmlib.MEGOLM_ALGORITHM;
|
||||
return sess;
|
||||
},
|
||||
),
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -717,11 +710,11 @@ Crypto.prototype.importRoomKeys = function(keys) {
|
||||
keys, (key) => {
|
||||
if (!key.room_id || !key.algorithm) {
|
||||
console.warn("ignoring room key entry with missing fields", key);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const alg = this._getRoomDecryptor(key.room_id, key.algorithm);
|
||||
alg.importRoomKey(key);
|
||||
return alg.importRoomKey(key);
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -771,8 +764,9 @@ Crypto.prototype.encryptEvent = function(event, room) {
|
||||
*
|
||||
* @param {MatrixEvent} event
|
||||
*
|
||||
* @return {Promise} resolves once we have finished decrypting. Rejects with an
|
||||
* `algorithms.DecryptionError` if there is a problem decrypting the event.
|
||||
* @return {Promise<module:crypto~EventDecryptionResult>} resolves once we have
|
||||
* finished decrypting. Rejects with an `algorithms.DecryptionError` if there
|
||||
* is a problem decrypting the event.
|
||||
*/
|
||||
Crypto.prototype.decryptEvent = function(event) {
|
||||
const content = event.getWireContent();
|
||||
@@ -781,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.
|
||||
@@ -833,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 +
|
||||
@@ -849,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) {
|
||||
@@ -859,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.
|
||||
@@ -880,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;
|
||||
@@ -910,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);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1239,7 +1234,7 @@ Crypto.prototype._signObject = async function(obj) {
|
||||
const sigs = {};
|
||||
sigs[this._userId] = {};
|
||||
sigs[this._userId]["ed25519:" + this._deviceId] =
|
||||
this._olmDevice.sign(anotherjson.stringify(obj));
|
||||
await this._olmDevice.sign(anotherjson.stringify(obj));
|
||||
obj.signatures = sigs;
|
||||
};
|
||||
|
||||
@@ -1294,6 +1289,27 @@ class IncomingRoomKeyRequestCancellation {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of a (successful) call to decryptEvent.
|
||||
*
|
||||
* @typedef {Object} EventDecryptionResult
|
||||
*
|
||||
* @property {Object} clearEvent The plaintext payload for the event
|
||||
* (typically containing <tt>type</tt> and <tt>content</tt> fields).
|
||||
*
|
||||
* @property {?string} senderCurve25519Key Key owned by the sender of this
|
||||
* event. See {@link module:models/event.MatrixEvent#getSenderKey}.
|
||||
*
|
||||
* @property {?string} claimedEd25519Key ed25519 key claimed by the sender of
|
||||
* this event. See
|
||||
* {@link module:models/event.MatrixEvent#getClaimedEd25519Key}.
|
||||
*
|
||||
* @property {?Array<string>} forwardingCurve25519KeyChain list of curve25519
|
||||
* keys involved in telling us about the senderCurve25519Key and
|
||||
* claimedEd25519Key. See
|
||||
* {@link module:models/event.MatrixEvent#getForwardingCurve25519KeyChain}.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when we receive a room key request
|
||||
*
|
||||
|
||||
@@ -58,7 +58,7 @@ module.exports.encryptMessageForDevice = async function(
|
||||
payloadFields,
|
||||
) {
|
||||
const deviceKey = recipientDevice.getIdentityKey();
|
||||
const sessionId = olmDevice.getSessionIdForDevice(deviceKey);
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(deviceKey);
|
||||
if (sessionId === null) {
|
||||
// If we don't have a session for a device then
|
||||
// we can't encrypt a message for it.
|
||||
@@ -102,7 +102,7 @@ module.exports.encryptMessageForDevice = async function(
|
||||
|
||||
utils.extend(payload, payloadFields);
|
||||
|
||||
resultsObject[deviceKey] = olmDevice.encryptMessage(
|
||||
resultsObject[deviceKey] = await olmDevice.encryptMessage(
|
||||
deviceKey, sessionId, JSON.stringify(payload),
|
||||
);
|
||||
};
|
||||
@@ -139,7 +139,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
|
||||
const deviceInfo = devices[j];
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
const sessionId = olmDevice.getSessionIdForDevice(key);
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(key);
|
||||
if (sessionId === null) {
|
||||
devicesWithoutSession.push([userId, deviceId]);
|
||||
}
|
||||
@@ -228,7 +228,7 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
|
||||
|
||||
let sid;
|
||||
try {
|
||||
sid = olmDevice.createOutboundSession(
|
||||
sid = await olmDevice.createOutboundSession(
|
||||
deviceInfo.getIdentityKey(), oneTimeKey.key,
|
||||
);
|
||||
} catch (e) {
|
||||
|
||||
@@ -293,7 +293,9 @@ InteractiveAuth.prototype = {
|
||||
},
|
||||
);
|
||||
if (!background) {
|
||||
prom = prom.catch(this._completionDeferred.reject);
|
||||
prom = prom.catch((e) => {
|
||||
this._completionDeferred.reject(e);
|
||||
});
|
||||
} else {
|
||||
// We ignore all failures here (even non-UI auth related ones)
|
||||
// since we don't want to suddenly fail if the internet connection
|
||||
|
||||
+122
-70
@@ -21,11 +21,9 @@ limitations under the License.
|
||||
* @module models/event
|
||||
*/
|
||||
|
||||
const EventEmitter = require("events").EventEmitter;
|
||||
|
||||
const utils = require('../utils.js');
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import {EventEmitter} from 'events';
|
||||
import utils from '../utils.js';
|
||||
|
||||
/**
|
||||
* Enum for event statuses.
|
||||
@@ -131,8 +129,10 @@ module.exports.MatrixEvent = function MatrixEvent(
|
||||
*/
|
||||
this._forwardingCurve25519KeyChain = [];
|
||||
|
||||
/* flag to indicate if we have a process decrypting this event */
|
||||
this._decrypting = false;
|
||||
/* if we have a process decrypting this event, a Promise which resolves
|
||||
* when it is finished. Normally null.
|
||||
*/
|
||||
this._decryptionPromise = null;
|
||||
|
||||
/* flag to indicate if we should retry decrypting this event after the
|
||||
* first attempt (eg, we have received new data which means that a second
|
||||
@@ -315,7 +315,20 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
* @return {boolean} True if this event is currently being decrypted, else false.
|
||||
*/
|
||||
isBeingDecrypted: function() {
|
||||
return this._decrypting;
|
||||
return this._decryptionPromise != null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if this event is an encrypted event which we failed to decrypt
|
||||
*
|
||||
* (This implies that we might retry decryption at some point in the future)
|
||||
*
|
||||
* @return {boolean} True if this event is an encrypted event which we
|
||||
* couldn't decrypt.
|
||||
*/
|
||||
isDecryptionFailure: function() {
|
||||
return this._clearEvent && this._clearEvent.content &&
|
||||
this._clearEvent.content.msgtype === "m.bad.encrypted";
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -326,13 +339,12 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
* @internal
|
||||
*
|
||||
* @param {module:crypto} crypto crypto module
|
||||
*
|
||||
* @returns {Promise} promise which resolves (to undefined) when the decryption
|
||||
* attempt is completed.
|
||||
*/
|
||||
attemptDecryption: function(crypto) {
|
||||
if (!crypto) {
|
||||
this._badEncryptedMessage("Encryption not enabled");
|
||||
return;
|
||||
}
|
||||
|
||||
attemptDecryption: async function(crypto) {
|
||||
// start with a couple of sanity checks.
|
||||
if (!this.isEncrypted()) {
|
||||
throw new Error("Attempt to decrypt event which isn't encrypted");
|
||||
}
|
||||
@@ -347,60 +359,112 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
);
|
||||
}
|
||||
|
||||
if (this._decrypting) {
|
||||
// if we already have a decryption attempt in progress, then it may
|
||||
// fail because it was using outdated info. We now have reason to
|
||||
// succeed where it failed before, but we don't want to have multiple
|
||||
// attempts going at the same time, so just set a flag that says we have
|
||||
// new info.
|
||||
//
|
||||
if (this._decryptionPromise) {
|
||||
console.log(
|
||||
`Event ${this.getId()} already being decrypted; queueing a retry`,
|
||||
);
|
||||
this._retryDecryption = true;
|
||||
return;
|
||||
return this._decryptionPromise;
|
||||
}
|
||||
|
||||
this._decrypting = true;
|
||||
|
||||
this._doDecryption(crypto).finally(() => {
|
||||
this._decrypting = false;
|
||||
this._retryDecryption = false;
|
||||
});
|
||||
this._decryptionPromise = this._decryptionLoop(crypto);
|
||||
return this._decryptionPromise;
|
||||
},
|
||||
|
||||
_doDecryption: function(crypto) {
|
||||
return Promise.try(() => {
|
||||
return crypto.decryptEvent(this);
|
||||
}).catch((e) => {
|
||||
if (e.name !== "DecryptionError") {
|
||||
// not a decryption error: log the whole exception as an error.
|
||||
console.error(
|
||||
`Error decrypting event (id=${this.getId()}): ${e.stack || e}`,
|
||||
);
|
||||
return null;
|
||||
} else if (this._retryDecryption) {
|
||||
// decryption error, but we have a retry queued.
|
||||
console.log(
|
||||
`Got error decrypting event (id=${this.getId()}), but retrying`,
|
||||
);
|
||||
this._retryDecryption = false;
|
||||
return this._doDecryption(crypto);
|
||||
} else {
|
||||
_decryptionLoop: async function(crypto) {
|
||||
// make sure that this method never runs completely synchronously.
|
||||
// (doing so would mean that we would clear _decryptionPromise *before*
|
||||
// it is set in attemptDecryption - and hence end up with a stuck
|
||||
// `_decryptionPromise`).
|
||||
await Promise.resolve();
|
||||
|
||||
while (true) {
|
||||
this._retryDecryption = false;
|
||||
|
||||
let res;
|
||||
try {
|
||||
if (!crypto) {
|
||||
res = this._badEncryptedMessage("Encryption not enabled");
|
||||
} else {
|
||||
res = await crypto.decryptEvent(this);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.name !== "DecryptionError") {
|
||||
// not a decryption error: log the whole exception as an error
|
||||
// (and don't bother with a retry)
|
||||
console.error(
|
||||
`Error decrypting event (id=${this.getId()}): ${e.stack || e}`,
|
||||
);
|
||||
this._decryptionPromise = null;
|
||||
this._retryDecryption = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// see if we have a retry queued.
|
||||
//
|
||||
// NB: make sure to keep this check in the same tick of the
|
||||
// event loop as `_decryptionPromise = null` below - otherwise we
|
||||
// risk a race:
|
||||
//
|
||||
// * A: we check _retryDecryption here and see that it is
|
||||
// false
|
||||
// * B: we get a second call to attemptDecryption, which sees
|
||||
// that _decryptionPromise is set so sets
|
||||
// _retryDecryption
|
||||
// * A: we continue below, clear _decryptionPromise, and
|
||||
// never do the retry.
|
||||
//
|
||||
if (this._retryDecryption) {
|
||||
// decryption error, but we have a retry queued.
|
||||
console.log(
|
||||
`Got error decrypting event (id=${this.getId()}: ` +
|
||||
`${e.message}), but retrying`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// decryption error, no retries queued. Warn about the error and
|
||||
// set it to m.bad.encrypted.
|
||||
console.warn(
|
||||
`Error decrypting event (id=${this.getId()}): ${e}`,
|
||||
);
|
||||
|
||||
this._badEncryptedMessage(e.message);
|
||||
return null;
|
||||
res = this._badEncryptedMessage(e.message);
|
||||
}
|
||||
});
|
||||
|
||||
// at this point, we've either successfully decrypted the event, or have given up
|
||||
// (and set res to a 'badEncryptedMessage'). Either way, we can now set the
|
||||
// cleartext of the event and raise Event.decrypted.
|
||||
//
|
||||
// make sure we clear '_decryptionPromise' before sending the 'Event.decrypted' event,
|
||||
// otherwise the app will be confused to see `isBeingDecrypted` still set when
|
||||
// there isn't an `Event.decrypted` on the way.
|
||||
//
|
||||
// see also notes on _retryDecryption above.
|
||||
//
|
||||
this._decryptionPromise = null;
|
||||
this._retryDecryption = false;
|
||||
this._setClearData(res);
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
_badEncryptedMessage: function(reason) {
|
||||
this.setClearData({
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: " + reason + " **",
|
||||
return {
|
||||
clearEvent: {
|
||||
type: "m.room.message",
|
||||
content: {
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: " + reason + " **",
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -412,29 +476,17 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
*
|
||||
* @fires module:models/event.MatrixEvent#"Event.decrypted"
|
||||
*
|
||||
* @param {Object} clearEvent The plaintext payload for the event
|
||||
* (typically containing <tt>type</tt> and <tt>content</tt> fields).
|
||||
*
|
||||
* @param {string=} senderCurve25519Key Key owned by the sender of this event.
|
||||
* See {@link module:models/event.MatrixEvent#getSenderKey}.
|
||||
*
|
||||
* @param {string=} claimedEd25519Key ed25519 key claimed by the sender of
|
||||
* this event. See {@link module:models/event.MatrixEvent#getClaimedEd25519Key}.
|
||||
*
|
||||
* @param {Array<string>=} forwardingCurve25519KeyChain list of curve25519 keys
|
||||
* involved in telling us about the senderCurve25519Key and claimedEd25519Key.
|
||||
* See {@link module:models/event.MatrixEvent#getForwardingCurve25519KeyChain}.
|
||||
* @param {module:crypto~EventDecryptionResult} decryptionResult
|
||||
* the decryption result, including the plaintext and some key info
|
||||
*/
|
||||
setClearData: function(
|
||||
clearEvent,
|
||||
senderCurve25519Key,
|
||||
claimedEd25519Key,
|
||||
forwardingCurve25519KeyChain,
|
||||
) {
|
||||
this._clearEvent = clearEvent;
|
||||
this._senderCurve25519Key = senderCurve25519Key || null;
|
||||
this._claimedEd25519Key = claimedEd25519Key || null;
|
||||
this._forwardingCurve25519KeyChain = forwardingCurve25519KeyChain || [];
|
||||
_setClearData: function(decryptionResult) {
|
||||
this._clearEvent = decryptionResult.clearEvent;
|
||||
this._senderCurve25519Key =
|
||||
decryptionResult.senderCurve25519Key || null;
|
||||
this._claimedEd25519Key =
|
||||
decryptionResult.claimedEd25519Key || null;
|
||||
this._forwardingCurve25519KeyChain =
|
||||
decryptionResult.forwardingCurve25519KeyChain || [];
|
||||
this.emit("Event.decrypted", this);
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
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 models/group
|
||||
*/
|
||||
const EventEmitter = require("events").EventEmitter;
|
||||
|
||||
const utils = require("../utils");
|
||||
|
||||
/**
|
||||
* Construct a new Group.
|
||||
*
|
||||
* @param {string} groupId The ID of this group.
|
||||
*
|
||||
* @prop {string} groupId The ID of this group.
|
||||
* @prop {string} name The human-readable display name for this group.
|
||||
* @prop {string} avatarUrl The mxc URL for this group's avatar.
|
||||
* @prop {string} myMembership The logged in user's membership of this group
|
||||
* @prop {Object} inviter Infomation about the user who invited the logged in user
|
||||
* to the group, if myMembership is 'invite'.
|
||||
* @prop {string} inviter.userId The user ID of the inviter
|
||||
*/
|
||||
function Group(groupId) {
|
||||
this.groupId = groupId;
|
||||
this.name = null;
|
||||
this.avatarUrl = null;
|
||||
this.myMembership = null;
|
||||
this.inviter = null;
|
||||
}
|
||||
utils.inherits(Group, EventEmitter);
|
||||
|
||||
Group.prototype.setProfile = function(name, avatarUrl) {
|
||||
if (this.name === name && this.avatarUrl === avatarUrl) return;
|
||||
|
||||
this.name = name || this.groupId;
|
||||
this.avatarUrl = avatarUrl;
|
||||
|
||||
this.emit("Group.profile", this);
|
||||
};
|
||||
|
||||
Group.prototype.setMyMembership = function(membership) {
|
||||
if (this.myMembership === membership) return;
|
||||
|
||||
this.myMembership = membership;
|
||||
|
||||
this.emit("Group.myMembership", this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the 'inviter' property. This does not emit an event (the inviter
|
||||
* will only change when the user is revited / reinvited to a room),
|
||||
* so set this before setting myMembership.
|
||||
* @param {Object} inviter Infomation about who invited us to the room
|
||||
*/
|
||||
Group.prototype.setInviter = function(inviter) {
|
||||
this.inviter = inviter;
|
||||
};
|
||||
|
||||
module.exports = Group;
|
||||
|
||||
/**
|
||||
* Fires whenever a group's profile information is updated.
|
||||
* This means the 'name' and 'avatarUrl' properties.
|
||||
* @event module:client~MatrixClient#"Group.profile"
|
||||
* @param {Group} group The group whose profile was updated.
|
||||
* @example
|
||||
* matrixClient.on("Group.profile", function(group){
|
||||
* var name = group.name;
|
||||
* });
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires whenever the logged in user's membership status of
|
||||
* the group is updated.
|
||||
* @event module:client~MatrixClient#"Group.myMembership"
|
||||
* @param {Group} group The group in which the user's membership changed
|
||||
* @example
|
||||
* matrixClient.on("Group.myMembership", function(group){
|
||||
* var myMembership = group.myMembership;
|
||||
* });
|
||||
*/
|
||||
+5
-3
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -175,6 +175,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
this._syncAccumulator.accumulate({
|
||||
next_batch: syncData.nextBatch,
|
||||
rooms: syncData.roomsData,
|
||||
groups: syncData.groupsData,
|
||||
account_data: {
|
||||
events: accountData,
|
||||
},
|
||||
@@ -251,7 +252,9 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
return Promise.all([
|
||||
this._persistUserPresenceEvents(userTuples),
|
||||
this._persistAccountData(syncData.accountData),
|
||||
this._persistSyncData(syncData.nextBatch, syncData.roomsData),
|
||||
this._persistSyncData(
|
||||
syncData.nextBatch, syncData.roomsData, syncData.groupsData,
|
||||
),
|
||||
]);
|
||||
},
|
||||
|
||||
@@ -259,9 +262,10 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
* Persist rooms /sync data along with the next batch token.
|
||||
* @param {string} nextBatch The next_batch /sync value.
|
||||
* @param {Object} roomsData The 'rooms' /sync data from a SyncAccumulator
|
||||
* @param {Object} groupsData The 'groups' /sync data from a SyncAccumulator
|
||||
* @return {Promise} Resolves if the data was persisted.
|
||||
*/
|
||||
_persistSyncData: function(nextBatch, roomsData) {
|
||||
_persistSyncData: function(nextBatch, roomsData, groupsData) {
|
||||
console.log("Persisting sync data up to ", nextBatch);
|
||||
return Promise.try(() => {
|
||||
const txn = this.db.transaction(["sync"], "readwrite");
|
||||
@@ -270,6 +274,7 @@ LocalIndexedDBStoreBackend.prototype = {
|
||||
clobber: "-", // constant key so will always clobber
|
||||
nextBatch: nextBatch,
|
||||
roomsData: roomsData,
|
||||
groupsData: groupsData,
|
||||
}); // put == UPSERT
|
||||
return promiseifyTxn(txn);
|
||||
});
|
||||
|
||||
+30
-2
@@ -19,8 +19,8 @@ limitations under the License.
|
||||
* This is an internal module. See {@link MatrixInMemoryStore} for the public class.
|
||||
* @module store/memory
|
||||
*/
|
||||
const utils = require("../utils");
|
||||
const User = require("../models/user");
|
||||
const utils = require("../utils");
|
||||
const User = require("../models/user");
|
||||
import Promise from 'bluebird';
|
||||
|
||||
/**
|
||||
@@ -35,6 +35,9 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
|
||||
this.rooms = {
|
||||
// roomId: Room
|
||||
};
|
||||
this.groups = {
|
||||
// groupId: Group
|
||||
};
|
||||
this.users = {
|
||||
// userId: User
|
||||
};
|
||||
@@ -69,6 +72,31 @@ module.exports.MatrixInMemoryStore.prototype = {
|
||||
this.syncToken = token;
|
||||
},
|
||||
|
||||
/**
|
||||
* Store the given room.
|
||||
* @param {Group} group The group to be stored
|
||||
*/
|
||||
storeGroup: function(group) {
|
||||
this.groups[group.groupId] = group;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a group by its group ID.
|
||||
* @param {string} groupId The group ID.
|
||||
* @return {Group} The group or null.
|
||||
*/
|
||||
getGroup: function(groupId) {
|
||||
return this.groups[groupId] || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve all known groups.
|
||||
* @return {Group[]} A list of groups, which may be empty.
|
||||
*/
|
||||
getGroups: function() {
|
||||
return utils.values(this.groups);
|
||||
},
|
||||
|
||||
/**
|
||||
* Store the given room.
|
||||
* @param {Room} room The room to be stored. All properties must be stored.
|
||||
|
||||
@@ -47,6 +47,30 @@ StubStore.prototype = {
|
||||
this.fromToken = token;
|
||||
},
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
* @param {Group} group
|
||||
*/
|
||||
storeGroup: function(group) {
|
||||
},
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
* @param {string} groupId
|
||||
* @return {null}
|
||||
*/
|
||||
getGroup: function(groupId) {
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
* @return {Array} An empty array.
|
||||
*/
|
||||
getGroups: function() {
|
||||
return [];
|
||||
},
|
||||
|
||||
/**
|
||||
* No-op.
|
||||
* @param {Room} room
|
||||
|
||||
@@ -72,10 +72,18 @@ class SyncAccumulator {
|
||||
// coherent /sync response and know at what point they should be
|
||||
// streaming from without losing events.
|
||||
this.nextBatch = null;
|
||||
|
||||
// { ('invite'|'join'|'leave'): $groupId: { ... sync 'group' data } }
|
||||
this.groups = {
|
||||
invite: {},
|
||||
join: {},
|
||||
leave: {},
|
||||
};
|
||||
}
|
||||
|
||||
accumulate(syncResponse) {
|
||||
this._accumulateRooms(syncResponse);
|
||||
this._accumulateGroups(syncResponse);
|
||||
this._accumulateAccountData(syncResponse);
|
||||
this.nextBatch = syncResponse.next_batch;
|
||||
}
|
||||
@@ -336,6 +344,44 @@ class SyncAccumulator {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accumulate incremental /sync group data.
|
||||
* @param {Object} syncResponse the complete /sync JSON
|
||||
*/
|
||||
_accumulateGroups(syncResponse) {
|
||||
if (!syncResponse.groups) {
|
||||
return;
|
||||
}
|
||||
if (syncResponse.groups.invite) {
|
||||
Object.keys(syncResponse.groups.invite).forEach((groupId) => {
|
||||
this._accumulateGroup(
|
||||
groupId, "invite", syncResponse.groups.invite[groupId],
|
||||
);
|
||||
});
|
||||
}
|
||||
if (syncResponse.groups.join) {
|
||||
Object.keys(syncResponse.groups.join).forEach((groupId) => {
|
||||
this._accumulateGroup(
|
||||
groupId, "join", syncResponse.groups.join[groupId],
|
||||
);
|
||||
});
|
||||
}
|
||||
if (syncResponse.groups.leave) {
|
||||
Object.keys(syncResponse.groups.leave).forEach((groupId) => {
|
||||
this._accumulateGroup(
|
||||
groupId, "leave", syncResponse.groups.leave[groupId],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_accumulateGroup(groupId, category, data) {
|
||||
for (const cat of ['invite', 'join', 'leave']) {
|
||||
delete this.groups[cat][groupId];
|
||||
}
|
||||
this.groups[category][groupId] = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return everything under the 'rooms' key from a /sync response which
|
||||
* represents all room data that should be stored. This should be paired
|
||||
@@ -470,6 +516,7 @@ class SyncAccumulator {
|
||||
return {
|
||||
nextBatch: this.nextBatch,
|
||||
roomsData: data,
|
||||
groupsData: this.groups,
|
||||
accountData: accData,
|
||||
};
|
||||
}
|
||||
|
||||
+200
-120
@@ -27,12 +27,11 @@ limitations under the License.
|
||||
import Promise from 'bluebird';
|
||||
const User = require("./models/user");
|
||||
const Room = require("./models/room");
|
||||
const Group = require('./models/group');
|
||||
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
|
||||
@@ -99,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"]);
|
||||
}
|
||||
}
|
||||
@@ -114,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",
|
||||
@@ -124,6 +123,17 @@ SyncApi.prototype.createRoom = function(roomId) {
|
||||
return room;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} groupId
|
||||
* @return {Group}
|
||||
*/
|
||||
SyncApi.prototype.createGroup = function(groupId) {
|
||||
const client = this.client;
|
||||
const group = new Group(groupId);
|
||||
client.reEmitter.reEmit(group, ["Group.profile", "Group.myMembership"]);
|
||||
return group;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Room} room
|
||||
* @private
|
||||
@@ -133,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",
|
||||
@@ -486,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;
|
||||
@@ -550,123 +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,
|
||||
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",
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -714,6 +734,19 @@ SyncApi.prototype._processSyncResponse = async function(syncToken, data) {
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// groups: {
|
||||
// invite: {
|
||||
// $groupId: {
|
||||
// inviter: $inviter,
|
||||
// profile: {
|
||||
// avatar_url: $avatarUrl,
|
||||
// name: $groupName,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// join: {},
|
||||
// leave: {},
|
||||
// },
|
||||
// }
|
||||
|
||||
// TODO-arch:
|
||||
@@ -781,6 +814,20 @@ SyncApi.prototype._processSyncResponse = async function(syncToken, data) {
|
||||
this._catchingUp = false;
|
||||
}
|
||||
|
||||
if (data.groups) {
|
||||
if (data.groups.invite) {
|
||||
this._processGroupSyncEntry(data.groups.invite, 'invite');
|
||||
}
|
||||
|
||||
if (data.groups.join) {
|
||||
this._processGroupSyncEntry(data.groups.join, 'join');
|
||||
}
|
||||
|
||||
if (data.groups.leave) {
|
||||
this._processGroupSyncEntry(data.groups.leave, 'leave');
|
||||
}
|
||||
}
|
||||
|
||||
// the returned json structure is a bit crap, so make it into a
|
||||
// nicer form (array) after applying sanity to make sure we don't fail
|
||||
// on missing keys (on the off chance)
|
||||
@@ -975,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
|
||||
@@ -1070,6 +1121,35 @@ SyncApi.prototype._pokeKeepAlive = function() {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} groupsSection Groups section object, eg. response.groups.invite
|
||||
* @param {string} sectionName Which section this is ('invite', 'join' or 'leave')
|
||||
*/
|
||||
SyncApi.prototype._processGroupSyncEntry = function(groupsSection, sectionName) {
|
||||
// Processes entries from 'groups' section of the sync stream
|
||||
for (const groupId of Object.keys(groupsSection)) {
|
||||
const groupInfo = groupsSection[groupId];
|
||||
let group = this.client.store.getGroup(groupId);
|
||||
const isBrandNew = group === null;
|
||||
if (group === null) {
|
||||
group = this.createGroup(groupId);
|
||||
}
|
||||
if (groupInfo.profile) {
|
||||
group.setProfile(
|
||||
groupInfo.profile.name, groupInfo.profile.avatar_url,
|
||||
);
|
||||
}
|
||||
if (groupInfo.inviter) {
|
||||
group.setInviter({userId: groupInfo.inviter});
|
||||
}
|
||||
group.setMyMembership(sectionName);
|
||||
if (isBrandNew) {
|
||||
this.client.store.storeGroup(group);
|
||||
this.client.emit("Group", group);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Object} obj
|
||||
* @return {Object[]}
|
||||
@@ -1255,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",
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user