Compare commits

..

67 Commits

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

There is plenty more that could be done here (self -> this, for instance), but
I've gone for keeping the diff minimal.
2017-09-08 11:53:10 +01:00
David Baker e07b304914 Merge pull request #532 from matrix-org/rav/eslint_es6
Add es6 to eslint environments
2017-09-07 10:33:52 +01:00
Richard van der Hoff 17364e72ec Add es6 to eslint environments 2017-09-07 10:29:27 +01:00
David Baker 22b213ae26 v0.8.2 2017-08-24 14:45:24 +01:00
David Baker 7d5936a9e9 Prepare changelog for v0.8.2 2017-08-24 14:45:24 +01:00
David Baker ab8f466f53 Merge pull request #530 from matrix-org/rav/fix_encrypted_calls
Handle m.call.* events which are decrypted asynchronously
2017-08-24 14:09:12 +01:00
David Baker 201177e7f0 Merge pull request #529 from matrix-org/dbkr/event_object_reemit
Re-emit events from, er, Event objects
2017-08-24 13:35:30 +01:00
Richard van der Hoff ec5f9a2892 Handle m.call.* events which are decrypted asynchronously
Handle the case where received m.call.* events are not decrypted at the point
of the 'event' notification by adding an 'Event.decrypted' listener for them.
2017-08-24 13:35:02 +01:00
Richard van der Hoff ee5b8748b5 Add MatrixEvent.isDecryptionFailure() 2017-08-24 13:35:02 +01:00
David Baker 8d04f8b8b5 Re-emit events from, er, Event objects
We do create Events in more places, but this is probably the only
place that matters since the only event is 'decrypted' which won't
fire for, eg. events we send.
2017-08-24 11:29:48 +01:00
David Baker 033babfbfc Groups: Sync Stream, Accept Invite & Leave (#528)
* WIP support for reading groups from sync stream

Only does invites currently

* More support for parsing groups in the sync stream

* Fix jsdoc
2017-08-24 10:24:24 +01:00
David Baker 15b77861ea v0.8.1 2017-08-23 15:51:31 +01:00
David Baker c4721850ce Prepare changelog for v0.8.1 2017-08-23 15:51:31 +01:00
David Baker b325aad5c9 v0.8.1-rc.1 2017-08-22 18:40:29 +01:00
David Baker 92e616f18e Prepare changelog for v0.8.1-rc.1 2017-08-22 18:40:29 +01:00
David Baker f7fee29c76 Merge pull request #527 from matrix-org/rav/fix_interactive_auth_error_handling
Fix error handling in interactive-auth
2017-08-21 16:43:19 +01:00
Richard van der Hoff eccea7411f Fix error handling in interactive-auth
Now that we are using bluebird, `defer.reject` is not implicitly bound, so we
need to call it properly rather than just passing it into the catch handler.

This fixes an error:

   promise.js:711 Uncaught TypeError: Cannot read property 'promise' of undefined
2017-08-21 16:31:42 +01:00
Michael Telatynski 01f93e0970 consume trailing slash when creating Matrix Client in HS and IS urls
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2017-08-19 11:00:46 +01:00
Richard van der Hoff 2d82a7bc2e Merge pull request #524 from matrix-org/rav/async_crypto/1
Make lots of OlmDevice asynchronous
2017-08-17 13:16:57 +01:00
Richard van der Hoff ca91fba071 Crypto test: Bump the timeout when waiting for Ali to claim keys
This failed a test, so let's just bump up the timeout a bit more.
2017-08-16 21:02:52 +01:00
Richard van der Hoff 9f2fce4d87 Try harder to wait for megolm decryption
Ok, this *really* ought to fix the racy test.
2017-08-16 19:01:47 +01:00
David Baker e1942267c5 Add API to invite & remove users from groups (#525)
* Add API invite & remove users from groups

* lint
2017-08-16 14:45:15 +01:00
Richard van der Hoff 12212409c7 Hopefully, fix racy megolm test
I couldn't repro the failure locally, but this looks like it should fix the
test failures.
2017-08-15 19:09:50 +01:00
Richard van der Hoff e5565c6bdb review comments 2017-08-15 18:34:04 +01:00
Richard van der Hoff f00558d840 Merge remote-tracking branch 'origin/develop' into rav/async_crypto/1 2017-08-15 18:31:02 +01:00
Richard van der Hoff da0dc5ed11 Merge pull request #523 from matrix-org/rav/fix_decryption_race
Make crypto.decryptMessage return decryption results
2017-08-15 18:07:33 +01:00
Richard van der Hoff e8be38ce5a Add delays to tests to wait for things to decrypt
Prepare for some refactoring which will add an extra tick to decryption by
adding some `awaitDecryption` calls in the integration tests.
2017-08-14 18:39:45 +01:00
Richard van der Hoff 38c9a05a0c Make Event.attemptDecryption return useful promises
Even if a decryption attempt is in progress, return a promise which blocks
until the attempt is complete.
2017-08-14 18:38:29 +01:00
Richard van der Hoff 110bd332f4 Make OlmDevice.exportInboundGroupSession async 2017-08-10 15:01:56 +01:00
Richard van der Hoff 8a0f73bf81 Make some OlmDevice megolm methods async
* OlmDevice.hasInboundSessionKeys
* OlmDevice.getInboundGroupSessionKey

The latter means that MegolmDecryption.shareKeysWithDevice takes longer before
it sends out the keyshare, so means the unit test needed an update
2017-08-10 15:01:56 +01:00
Richard van der Hoff 337c9cbea3 Make OlmDevice.decryptGroupMessage async 2017-08-10 15:01:56 +01:00
Richard van der Hoff cfd61096d9 Make OlmDevice.importInboundGroupSession async 2017-08-10 15:01:56 +01:00
Richard van der Hoff 2894e253a2 Make OlmDevice.addInboundGroupSession async 2017-08-10 15:01:56 +01:00
Richard van der Hoff e52985e082 Olm session creation async 2017-08-10 15:01:56 +01:00
Richard van der Hoff 7d2bc12bb7 Make OlmDevice key generation async
* OlmDevice.generateOneTimeKeys becomes async
* Stash maxOneTimeKeys at init so that maxNumberOfOneTimeKeys can remain sync
2017-08-10 15:01:56 +01:00
Richard van der Hoff a5f397b26d OlmDevice.oneTimeKeys async
* OlmDevice.getOneTimeKeys
* OlmDevice.markKeysAsPublished
2017-08-10 15:01:56 +01:00
Richard van der Hoff 5b93d5210e Make OlmDevice.sign async 2017-08-10 15:01:56 +01:00
Richard van der Hoff e943a6e09c Make OlmDevice olmSession methods asynchronous
* OlmDevice.encryptMessage
* OlmDevice.decryptMessage
* OlmDevice.matchesSession
2017-08-10 15:01:56 +01:00
Richard van der Hoff 8f527a6212 make session tracking methods in OlmDevice async
* OlmDevice.getSessionIdsForDevice
* OlmDevice.getSessionIdForDevice
* OlmDevice.getSessionInfoForDevice
2017-08-10 15:01:56 +01:00
Richard van der Hoff f2f8ad6b65 Make OlmDevice initialisation asynchronous
Add an asynchronous `init` method to OlmDevice which initialises the OlmAccount.
2017-08-10 15:01:56 +01:00
Richard van der Hoff c870930bc0 Add delays to tests to wait for things to decrypt
Prepare for some refactoring which will add an extra tick to decryption by
adding some `awaitDecryption` calls in the integration tests.
2017-08-10 15:01:56 +01:00
Richard van der Hoff b26b1caa86 fix jsdoc 2017-08-10 14:56:42 +01:00
Richard van der Hoff 6613ee6b0d Make crypto.decryptMessage return decryption results
... instead of having it call event.setClearData.

The main advantage of this is that it fixes a race condition, wherein apps
could see `event.isDecrypting()` to be true, but in fact the event had been
decrypted (and there was no `Event.decrypted` event on its way).

We're also fixing another race, wherein if the first attempt to decrypt failed,
a call to `attemptDecryption` would race against the first call and a second
attempt to decrypt would never happen.

This also gives a cleaner interface to MatrixEvent, at the expense of making
the `megolm` unit test a bit more hoop-jumpy.
2017-08-10 13:05:35 +01:00
Richard van der Hoff 9550bca099 Megolm: remove redundant requestKeysOnFail
We now *always* requestKeysOnFail, so this was dead code which we can remove.
2017-08-10 13:00:27 +01:00
30 changed files with 1647 additions and 648 deletions
+4
View File
@@ -9,6 +9,10 @@ module.exports = {
env: {
browser: true,
node: true,
// babel's transform-runtime converts references to ES6 globals such as
// Promise and Map to core-js polyfills, so we can use ES6 globals.
es6: true,
},
extends: ["eslint:recommended", "google"],
rules: {
+49
View File
@@ -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
View File
@@ -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
View File
@@ -33,12 +33,20 @@ import Promise from 'bluebird';
* @param {string} userId
* @param {string} deviceId
* @param {string} accessToken
*
* @param {WebStorage=} sessionStoreBackend a web storage object to use for the
* session store. If undefined, we will create a MockStorageApi.
*/
export default function TestClient(userId, deviceId, accessToken) {
export default function TestClient(
userId, deviceId, accessToken, sessionStoreBackend,
) {
this.userId = userId;
this.deviceId = deviceId;
this.storage = new sdk.WebStorageSessionStore(new testUtils.MockStorageApi());
if (sessionStoreBackend === undefined) {
sessionStoreBackend = new testUtils.MockStorageApi();
}
this.storage = new sdk.WebStorageSessionStore(sessionStoreBackend);
this.httpBackend = new MockHttpBackend();
this.client = sdk.createClient({
baseUrl: "http://" + userId + ".test.server",
+369
View File
@@ -0,0 +1,369 @@
import expect from 'expect';
import Promise from 'bluebird';
import TestClient from '../TestClient';
import testUtils from '../test-utils';
const ROOM_ID = "!room:id";
/**
* get a /sync response which contains a single e2e room (ROOM_ID), with the
* members given
*
* @param {string[]} roomMembers
*
* @return {object} sync response
*/
function getSyncResponse(roomMembers) {
const stateEvents = [
testUtils.mkEvent({
type: 'm.room.encryption',
skey: '',
content: {
algorithm: 'm.megolm.v1.aes-sha2',
},
}),
];
Array.prototype.push.apply(
stateEvents,
roomMembers.map(
(m) => testUtils.mkMembership({
mship: 'join',
sender: m,
}),
),
);
const syncResponse = {
next_batch: 1,
rooms: {
join: {
[ROOM_ID]: {
state: {
events: stateEvents,
},
},
},
},
};
return syncResponse;
}
describe("DeviceList management:", function() {
if (!global.Olm) {
console.warn('not running deviceList tests: Olm not present');
return;
}
let sessionStoreBackend;
let aliceTestClient;
async function createTestClient() {
const testClient = new TestClient(
"@alice:localhost", "xzcvb", "akjgkrgjs", sessionStoreBackend,
);
await testClient.client.initCrypto();
return testClient;
}
beforeEach(async function() {
testUtils.beforeEach(this); // eslint-disable-line no-invalid-this
// we create our own sessionStoreBackend so that we can use it for
// another TestClient.
sessionStoreBackend = new testUtils.MockStorageApi();
aliceTestClient = await createTestClient();
});
afterEach(function() {
aliceTestClient.stop();
});
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
return aliceTestClient.start().then(function() {
const syncResponse = getSyncResponse(['@bob:xyz']);
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
return aliceTestClient.flushSync();
}).then(function() {
console.log("Forcing alice to download our device keys");
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(200, {
device_keys: {
'@bob:xyz': {},
},
});
return Promise.all([
aliceTestClient.client.downloadKeys(['@bob:xyz']),
aliceTestClient.httpBackend.flush('/keys/query', 1),
]);
}).then(function() {
console.log("Telling alice to send a megolm message");
aliceTestClient.httpBackend.when(
'PUT', '/send/',
).respond(200, {
event_id: '$event_id',
});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
// the crypto stuff can take a while, so give the requests a whole second.
aliceTestClient.httpBackend.flushAllExpected({
timeout: 1000,
}),
]);
});
});
it("We should not get confused by out-of-order device query responses",
() => {
// https://github.com/vector-im/riot-web/issues/3126
return aliceTestClient.start().then(() => {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
return aliceTestClient.flushSync();
}).then(() => {
// to make sure the initial device queries are flushed out, we
// attempt to send a message.
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {
device_keys: {
'@bob:xyz': {},
'@chris:abc': {},
},
},
);
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
200, {event_id: '$event1'});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
() => aliceTestClient.httpBackend.flush('/send/', 1),
),
]);
}).then(() => {
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(1);
// invalidate bob's and chris's device lists in separate syncs
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '2',
device_lists: {
changed: ['@bob:xyz'],
},
});
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, {
next_batch: '3',
device_lists: {
changed: ['@chris:abc'],
},
});
// flush both syncs
return aliceTestClient.flushSync().then(() => {
return aliceTestClient.flushSync();
});
}).then(() => {
// check that we don't yet have a request for chris's devices.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@chris:abc': {},
},
token: '3',
}).respond(200, {
device_keys: {'@chris:abc': {}},
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(0);
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
if (bobStat != 1 && bobStat != 2) {
throw new Error('Unexpected status for bob: wanted 1 or 2, got ' +
bobStat);
}
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
chrisStat);
}
// now add an expectation for a query for bob's devices, and let
// it complete.
aliceTestClient.httpBackend.when('POST', '/keys/query', {
device_keys: {
'@bob:xyz': {},
},
token: '2',
}).respond(200, {
device_keys: {'@bob:xyz': {}},
});
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
}).then(() => {
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toEqual(3);
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
if (chrisStat != 1 && chrisStat != 2) {
throw new Error('Unexpected status for chris: wanted 1 or 2, got ' +
bobStat);
}
// now let the query for chris's devices complete.
return aliceTestClient.httpBackend.flush('/keys/query', 1);
}).then((flushed) => {
expect(flushed).toEqual(1);
// wait for the client to stop processing the response
return aliceTestClient.client.downloadKeys(['@chris:abc']);
}).then(() => {
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
const chrisStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@chris:abc'];
expect(bobStat).toEqual(3);
expect(chrisStat).toEqual(3);
expect(aliceTestClient.storage.getEndToEndDeviceSyncToken()).toEqual(3);
});
});
// https://github.com/vector-im/riot-web/issues/4983
describe("Alice should know she has stale device lists", () => {
beforeEach(async function() {
await aliceTestClient.start();
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse(['@bob:xyz']));
await aliceTestClient.flushSync();
aliceTestClient.httpBackend.when('POST', '/keys/query').respond(
200, {
device_keys: {
'@bob:xyz': {},
},
},
);
await aliceTestClient.httpBackend.flush('/keys/query', 1);
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toBeGreaterThan(
0, "Alice should be tracking bob's device list",
);
});
it("when Bob leaves", async function() {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, {
next_batch: 2,
device_lists: {
left: ['@bob:xyz'],
},
rooms: {
join: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: 'leave',
sender: '@bob:xyz',
}),
],
},
},
},
},
},
);
await aliceTestClient.flushSync();
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
});
it("when Alice leaves", async function() {
aliceTestClient.httpBackend.when('GET', '/sync').respond(
200, {
next_batch: 2,
device_lists: {
left: ['@bob:xyz'],
},
rooms: {
leave: {
[ROOM_ID]: {
timeline: {
events: [
testUtils.mkMembership({
mship: 'leave',
sender: '@bob:xyz',
}),
],
},
},
},
},
},
);
await aliceTestClient.flushSync();
const bobStat = aliceTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
});
it("when Bob leaves whilst Alice is offline", async function() {
aliceTestClient.stop();
const anotherTestClient = await createTestClient();
try {
anotherTestClient.httpBackend.when('GET', '/keys/changes').respond(
200, {
changed: [],
left: ['@bob:xyz'],
},
);
await anotherTestClient.start();
anotherTestClient.httpBackend.when('GET', '/sync').respond(
200, getSyncResponse([]));
await anotherTestClient.flushSync();
const bobStat = anotherTestClient.storage
.getEndToEndDeviceTrackingStatus()['@bob:xyz'];
expect(bobStat).toEqual(
0, "Alice should have marked bob's device list as untracked",
);
} finally {
anotherTestClient.stop();
}
});
});
});
+41 -37
View File
@@ -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
View File
@@ -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;
+22
View File
@@ -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);
});
});
};
+31 -15
View File
@@ -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.
+82
View File
@@ -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;
});
});
});
});
+46
View File
@@ -0,0 +1,46 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module
*/
export default class Reemitter {
constructor(target) {
this.target = target;
// We keep one bound event handler for each event name so we know
// what event is arriving
this.boundHandlers = {};
}
_handleEvent(eventName, ...args) {
this.target.emit(eventName, ...args);
}
reEmit(source, eventNames) {
for (const eventName of eventNames) {
if (this.boundHandlers[eventName] === undefined) {
this.boundHandlers[eventName] = this._handleEvent.bind(this, eventName);
}
const boundHandler = this.boundHandlers[eventName];
source.on(eventName, boundHandler);
}
}
}
+84
View File
@@ -411,6 +411,16 @@ MatrixBaseApis.prototype.getGroupSummary = function(groupId) {
return this._http.authedRequest(undefined, "GET", path);
};
/**
* @param {string} groupId
* @return {module:client.Promise} Resolves: Group profile object
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getGroupProfile = function(groupId) {
const path = utils.encodeUri("/groups/$groupId/profile", {$groupId: groupId});
return this._http.authedRequest(undefined, "GET", path);
};
/**
* @param {string} groupId
* @param {Object} profile The group profile object
@@ -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
View File
@@ -40,7 +40,7 @@ const SyncApi = require("./sync");
const MatrixBaseApis = require("./base-apis");
const MatrixError = httpApi.MatrixError;
import reEmit from './reemit';
import ReEmitter from './ReEmitter';
const SCROLLBACK_DELAY_MS = 3000;
let CRYPTO_ENABLED = false;
@@ -113,8 +113,20 @@ try {
* crypto store implementation.
*/
function MatrixClient(opts) {
// Allow trailing slash in HS url
if (opts.baseUrl && opts.baseUrl.endsWith("/")) {
opts.baseUrl = opts.baseUrl.substr(0, opts.baseUrl.length - 1);
}
// Allow trailing slash in IS url
if (opts.idBaseUrl && opts.idBaseUrl.endsWith("/")) {
opts.idBaseUrl = opts.idBaseUrl.substr(0, opts.idBaseUrl.length - 1);
}
MatrixBaseApis.call(this, opts);
this.reEmitter = new ReEmitter(this);
this.store = opts.store || new StubStore();
this.deviceId = opts.deviceId || null;
@@ -364,7 +376,7 @@ MatrixClient.prototype.initCrypto = async function() {
this._cryptoStore,
);
reEmit(this, crypto, [
this.reEmitter.reEmit(crypto, [
"crypto.roomKeyRequest",
"crypto.roomKeyRequestCancellation",
]);
@@ -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
+43 -4
View File
@@ -26,8 +26,30 @@ import Promise from 'bluebird';
import DeviceInfo from './deviceinfo';
import olmlib from './olmlib';
/* State transition diagram for DeviceList._deviceTrackingStatus
*
* |
* stopTrackingDeviceList V
* +---------------------> NOT_TRACKED
* | |
* +<--------------------+ | startTrackingDeviceList
* | | V
* | +-------------> PENDING_DOWNLOAD <--------------------+-+
* | | ^ | | |
* | | restart download | | start download | | invalidateUserDeviceList
* | | client failed | | | |
* | | | V | |
* | +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
* | | | |
* +<-------------------+ | download successful |
* ^ V |
* +----------------------- UP_TO_DATE ------------------------+
*/
// constants for DeviceList._deviceTrackingStatus
// const TRACKING_STATUS_NOT_TRACKED = 0;
const TRACKING_STATUS_NOT_TRACKED = 0;
const TRACKING_STATUS_PENDING_DOWNLOAD = 1;
const TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2;
const TRACKING_STATUS_UP_TO_DATE = 3;
@@ -236,6 +258,26 @@ export default class DeviceList {
// refreshOutdatedDeviceLists.
}
/**
* Mark the given user as no longer being tracked for device-list updates.
*
* This won't affect any in-progress downloads, which will still go on to
* complete; it will just mean that we don't think that we have an up-to-date
* list for future calls to downloadKeys.
*
* @param {String} userId
*/
stopTrackingDeviceList(userId) {
if (this._deviceTrackingStatus[userId]) {
console.log('No longer tracking device list for ' + userId);
this._deviceTrackingStatus[userId] = TRACKING_STATUS_NOT_TRACKED;
}
// we don't yet persist the tracking status, since there may be a lot
// of calls; instead we wait for the forthcoming
// refreshOutdatedDeviceLists.
}
/**
* Mark the cached device list for the given user outdated.
*
@@ -283,9 +325,6 @@ export default class DeviceList {
usersToDownload.push(userId);
}
}
if (usersToDownload.length == 0) {
return;
}
// we didn't persist the tracking status during
// invalidateUserDeviceList, so do it now.
+65 -46
View File
@@ -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,
);
+2 -1
View File
@@ -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.
*/
+32 -36
View File
@@ -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,
);
+16 -8
View File
@@ -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
View File
@@ -69,7 +69,6 @@ function Crypto(baseApis, sessionStore, userId, deviceId,
this._olmDevice = new OlmDevice(sessionStore);
this._deviceList = new DeviceList(baseApis, sessionStore, this._olmDevice);
this._initialDeviceListInvalidationPending = false;
// the last time we did a check for the number of one-time-keys on the
// server.
@@ -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
*
+4 -4
View File
@@ -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) {
+3 -1
View File
@@ -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
View File
@@ -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);
},
+95
View File
@@ -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
View File
@@ -27,7 +27,7 @@ const ContentRepo = require("../content-repo");
const EventTimeline = require("./event-timeline");
const EventTimelineSet = require("./event-timeline-set");
import reEmit from '../reemit';
import ReEmitter from '../ReEmitter';
function synthesizeReceipt(userId, event, receiptType) {
// console.log("synthesizing receipt for "+event.getId());
@@ -106,6 +106,8 @@ function Room(roomId, opts) {
opts = opts || {};
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
this.reEmitter = new ReEmitter(this);
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
throw new Error(
"opts.pendingEventOrdering MUST be either 'chronological' or " +
@@ -153,7 +155,7 @@ function Room(roomId, opts) {
// all our per-room timeline sets. the first one is the unfiltered ones;
// the subsequent ones are the filtered ones in no particular order.
this._timelineSets = [new EventTimelineSet(this, opts)];
reEmit(this, this.getUnfilteredTimelineSet(),
this.reEmitter.reEmit(this.getUnfilteredTimelineSet(),
["Room.timeline", "Room.timelineReset"]);
this._fixUpLegacyTimelineFields();
@@ -490,7 +492,7 @@ Room.prototype.getOrCreateFilteredTimelineSet = function(filter) {
}
const opts = Object.assign({ filter: filter }, this._opts);
const timelineSet = new EventTimelineSet(this, opts);
reEmit(this, timelineSet, ["Room.timeline", "Room.timelineReset"]);
this.reEmitter.reEmit(timelineSet, ["Room.timeline", "Room.timelineReset"]);
this._filteredTimelineSets[filter.filterId] = timelineSet;
this._timelineSets.push(timelineSet);
-44
View File
@@ -1,44 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @module
*/
/**
* re-emit events raised by one EventEmitter from another
*
* @param {external:EventEmitter} reEmitEntity
* entity from which we want events to be emitted
* @param {external:EventEmitter} emittableEntity
* entity from which events are currently emitted
* @param {Array<string>} eventNames
* list of events to be reemitted
*/
export default function reEmit(reEmitEntity, emittableEntity, eventNames) {
for (const eventName of eventNames) {
// setup a listener on the entity (the Room, User, etc) for this event
emittableEntity.on(eventName, function(...args) {
// take the args from the listener and reuse them, adding the
// event name to the arg list so it works with .emit()
// Transformation Example:
// listener on "foo" => function(a,b) { ... }
// Re-emit on "thing" => thing.emit("foo", a, b)
reEmitEntity.emit(eventName, ...args);
});
}
}
+7 -2
View File
@@ -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
View File
@@ -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.
+24
View File
@@ -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
+47
View File
@@ -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
View File
@@ -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",
]);