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

Merge remote-tracking branch 'upstream/dbkr/e2e_backups' into e2e_backups

This commit is contained in:
Hubert Chathi
2018-10-11 14:01:26 -04:00
24 changed files with 453 additions and 163 deletions

View File

@@ -1,3 +1,38 @@
Changes in [0.11.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.1) (2018-10-01)
==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.1-rc.1...v0.11.1)
* No changes since rc.1
Changes in [0.11.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.1-rc.1) (2018-09-27)
============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.0...v0.11.1-rc.1)
* make usage of hub compatible with latest version (2.5)
[\#747](https://github.com/matrix-org/matrix-js-sdk/pull/747)
* Detect when lazy loading has been toggled in client.startClient
[\#746](https://github.com/matrix-org/matrix-js-sdk/pull/746)
* Add getMediaLimits to client
[\#644](https://github.com/matrix-org/matrix-js-sdk/pull/644)
* Split npm start into an init and watch script
[\#742](https://github.com/matrix-org/matrix-js-sdk/pull/742)
* Revert "room name should only take canonical alias into account"
[\#738](https://github.com/matrix-org/matrix-js-sdk/pull/738)
* fix display name disambiguation with LL
[\#737](https://github.com/matrix-org/matrix-js-sdk/pull/737)
* Introduce Room.myMembership event
[\#735](https://github.com/matrix-org/matrix-js-sdk/pull/735)
* room name should only take canonical alias into account
[\#733](https://github.com/matrix-org/matrix-js-sdk/pull/733)
* state events from context response were not wrapped in a MatrixEvent
[\#732](https://github.com/matrix-org/matrix-js-sdk/pull/732)
* Reduce amount of promises created when inserting members
[\#724](https://github.com/matrix-org/matrix-js-sdk/pull/724)
* dont wait for LL members to be stored to resolve the members
[\#726](https://github.com/matrix-org/matrix-js-sdk/pull/726)
* RoomState.members emitted with wrong argument order for OOB members
[\#728](https://github.com/matrix-org/matrix-js-sdk/pull/728)
Changes in [0.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (2018-09-10) Changes in [0.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v0.11.0) (2018-09-10)
================================================================================================== ==================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.0-rc.1...v0.11.0) [Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v0.11.0-rc.1...v0.11.0)

View File

@@ -1,6 +1,6 @@
{ {
"name": "matrix-js-sdk", "name": "matrix-js-sdk",
"version": "0.11.0", "version": "0.11.1",
"description": "Matrix Client-Server SDK for Javascript", "description": "Matrix Client-Server SDK for Javascript",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -10,7 +10,9 @@
"test": "npm run test:build && npm run test:run", "test": "npm run test:build && npm run test:run",
"check": "npm run test:build && _mocha --recursive specbuild --colors", "check": "npm run test:build && _mocha --recursive specbuild --colors",
"gendoc": "babel --no-babelrc -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc", "gendoc": "babel --no-babelrc -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
"start": "babel -s -w -d lib src", "start": "npm run start:init && npm run start:watch",
"start:watch": "babel -s -w --skip-initial-build -d lib src",
"start:init": "babel -s -d lib src",
"clean": "rimraf lib dist", "clean": "rimraf lib dist",
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && uglifyjs -c -m -o dist/browser-matrix.min.js --source-map dist/browser-matrix.min.js.map --in-source-map dist/browser-matrix.js.map dist/browser-matrix.js", "build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && uglifyjs -c -m -o dist/browser-matrix.min.js --source-map dist/browser-matrix.min.js.map --in-source-map dist/browser-matrix.js.map dist/browser-matrix.js",
"dist": "npm run build", "dist": "npm run build",
@@ -53,6 +55,7 @@
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"bluebird": "^3.5.0", "bluebird": "^3.5.0",
"browser-request": "^0.3.3", "browser-request": "^0.3.3",
"bs58": "^4.0.1",
"content-type": "^1.0.2", "content-type": "^1.0.2",
"request": "^2.53.0" "request": "^2.53.0"
}, },
@@ -72,8 +75,8 @@
"jsdoc": "^3.5.5", "jsdoc": "^3.5.5",
"lolex": "^1.5.2", "lolex": "^1.5.2",
"matrix-mock-request": "^1.2.0", "matrix-mock-request": "^1.2.0",
"mocha": "^3.2.0", "mocha": "^5.2.0",
"mocha-jenkins-reporter": "^0.3.6", "mocha-jenkins-reporter": "^0.4.0",
"rimraf": "^2.5.4", "rimraf": "^2.5.4",
"source-map-support": "^0.4.11", "source-map-support": "^0.4.11",
"sourceify": "^0.1.0", "sourceify": "^0.1.0",

View File

@@ -245,7 +245,7 @@ release_text=`mktemp`
echo "$tag" > "${release_text}" echo "$tag" > "${release_text}"
echo >> "${release_text}" echo >> "${release_text}"
cat "${latest_changes}" >> "${release_text}" cat "${latest_changes}" >> "${release_text}"
hub release create $hubflags $assets -f "${release_text}" "$tag" hub release create $hubflags $assets -F "${release_text}" "$tag"
if [ $dodist -eq 0 ]; then if [ $dodist -eq 0 ]; then
rm -rf "$builddir" rm -rf "$builddir"
@@ -281,7 +281,7 @@ fi
echo "updating master branch" echo "updating master branch"
git checkout master git checkout master
git pull git pull
git merge --ff-only "$rel_branch" git merge "$rel_branch"
# push master and docs (if generated) to github # push master and docs (if generated) to github
git push origin master git push origin master

View File

@@ -94,7 +94,7 @@ describe("MatrixClient opts", function() {
httpBackend.flush("/txn1", 1); httpBackend.flush("/txn1", 1);
}); });
it("should be able to sync / get new events", function(done) { it("should be able to sync / get new events", async function() {
const expectedEventTypes = [ // from /initialSync const expectedEventTypes = [ // from /initialSync
"m.room.message", "m.room.name", "m.room.member", "m.room.member", "m.room.message", "m.room.name", "m.room.member", "m.room.member",
"m.room.create", "m.room.create",
@@ -110,20 +110,16 @@ describe("MatrixClient opts", function() {
httpBackend.when("GET", "/pushrules").respond(200, {}); httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" }); httpBackend.when("POST", "/filter").respond(200, { filter_id: "foo" });
httpBackend.when("GET", "/sync").respond(200, syncData); httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient(); await client.startClient();
httpBackend.flush("/pushrules", 1).then(function() { await httpBackend.flush("/pushrules", 1);
return httpBackend.flush("/filter", 1); await httpBackend.flush("/filter", 1);
}).then(function() { await Promise.all([
return Promise.all([ httpBackend.flush("/sync", 1),
httpBackend.flush("/sync", 1), utils.syncPromise(client),
utils.syncPromise(client), ]);
]); expect(expectedEventTypes.length).toEqual(
}).done(function() { 0, "Expected to see event types: " + expectedEventTypes,
expect(expectedEventTypes.length).toEqual( );
0, "Expected to see event types: " + expectedEventTypes,
);
done();
});
}); });
}); });

View File

@@ -1,20 +1,24 @@
"use strict"; "use strict";
import 'source-map-support/register'; import 'source-map-support/register';
import Crypto from '../../lib/crypto';
import expect from 'expect';
const sdk = require("../.."); const sdk = require("../..");
let Crypto;
if (sdk.CRYPTO_ENABLED) {
Crypto = require("../../lib/crypto");
}
import expect from 'expect'; const Olm = global.Olm;
describe("Crypto", function() { describe("Crypto", function() {
if (!sdk.CRYPTO_ENABLED) { if (!sdk.CRYPTO_ENABLED) {
return; return;
} }
beforeEach(function(done) {
Olm.init().then(done);
});
it("Crypto exposes the correct olm library version", function() { it("Crypto exposes the correct olm library version", function() {
console.log(Crypto);
expect(Crypto.getOlmVersion()[0]).toEqual(2); expect(Crypto.getOlmVersion()[0]).toEqual(2);
}); });
}); });

View File

@@ -13,20 +13,16 @@ import WebStorageSessionStore from '../../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js'; import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../../MockStorageApi'; import MockStorageApi from '../../../MockStorageApi';
import testUtils from '../../../test-utils'; import testUtils from '../../../test-utils';
import OlmDevice from '../../../../lib/crypto/OlmDevice';
// Crypto and OlmDevice won't import unless we have global.Olm import Crypto from '../../../../lib/crypto';
let OlmDevice;
let Crypto;
if (global.Olm) {
OlmDevice = require('../../../../lib/crypto/OlmDevice');
Crypto = require('../../../../lib/crypto');
}
const MatrixEvent = sdk.MatrixEvent; const MatrixEvent = sdk.MatrixEvent;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2']; const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const ROOM_ID = '!ROOM:ID'; const ROOM_ID = '!ROOM:ID';
const Olm = global.Olm;
describe("MegolmDecryption", function() { describe("MegolmDecryption", function() {
if (!global.Olm) { if (!global.Olm) {
console.warn('Not running megolm unit tests: libolm not present'); console.warn('Not running megolm unit tests: libolm not present');
@@ -69,7 +65,8 @@ describe("MegolmDecryption", function() {
describe('receives some keys:', function() { describe('receives some keys:', function() {
let groupSession; let groupSession;
beforeEach(function() { beforeEach(async function() {
await Olm.init();
groupSession = new global.Olm.OutboundGroupSession(); groupSession = new global.Olm.OutboundGroupSession();
groupSession.create(); groupSession.create();
@@ -98,7 +95,7 @@ describe("MegolmDecryption", function() {
}, },
}; };
return event.attemptDecryption(mockCrypto).then(() => { await event.attemptDecryption(mockCrypto).then(() => {
megolmDecryption.onRoomKeyEvent(event); megolmDecryption.onRoomKeyEvent(event);
}); });
}); });

View File

@@ -43,13 +43,14 @@ const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const ROOM_ID = '!ROOM:ID'; const ROOM_ID = '!ROOM:ID';
const SESSION_ID = 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc';
const ENCRYPTED_EVENT = new MatrixEvent({ const ENCRYPTED_EVENT = new MatrixEvent({
type: 'm.room.encrypted', type: 'm.room.encrypted',
room_id: '!ROOM:ID', room_id: '!ROOM:ID',
content: { content: {
algorithm: 'm.megolm.v1.aes-sha2', algorithm: 'm.megolm.v1.aes-sha2',
sender_key: 'SENDER_CURVE25519', sender_key: 'SENDER_CURVE25519',
session_id: 'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', session_id: SESSION_ID,
ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N' ciphertext: 'AwgAEjD+VwXZ7PoGPRS/H4kwpAsMp/g+WPvJVtPEKE8fmM9IcT/N'
+ 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl' + 'CiwPb8PehecDKP0cjm1XO88k6Bw3D17aGiBHr5iBoP7oSw8CXULXAMTkBl'
+ 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs', + 'mkufRQq2+d0Giy1s4/Cg5n13jSVrSb2q7VTSv1ZHAFjUCsLSfR0gxqcQs',
@@ -412,7 +413,7 @@ describe("MegolmBackup", function() {
"qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD" "qx37WTQrjZLz5tId/uBX9B3/okqAbV1ofl9UnHKno1eipByCpXleAAlAZoJgYnCDOQZD"
+ "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA", + "QWzo3luTSfkF9pU1mOILCbbouubs6TVeDyPfgGD9i86J8irHjA",
ROOM_ID, ROOM_ID,
'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc', SESSION_ID,
).then(() => { ).then(() => {
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
}).then((res) => { }).then((res) => {
@@ -426,10 +427,10 @@ describe("MegolmBackup", function() {
rooms: { rooms: {
[ROOM_ID]: { [ROOM_ID]: {
sessions: { sessions: {
'o+21hSjP+mgEmcfdslPsQdvzWnkdt0Wyo00Kp++R8Kc': KEY_BACKUP_DATA, SESSION_ID: KEY_BACKUP_DATA,
}, },
}, },
} },
}); });
}; };
return client.restoreKeyBackups( return client.restoreKeyBackups(

View File

@@ -139,6 +139,9 @@ describe("MatrixClient", function() {
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null)); store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null)); store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null)); store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
store.getClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
store.storeClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
store.isNewlyCreated = expect.createSpy().andReturn(Promise.resolve(true));
client = new MatrixClient({ client = new MatrixClient({
baseUrl: "https://my.home.server", baseUrl: "https://my.home.server",
idBaseUrl: identityServerUrl, idBaseUrl: identityServerUrl,
@@ -182,7 +185,7 @@ describe("MatrixClient", function() {
}); });
}); });
it("should not POST /filter if a matching filter already exists", function(done) { it("should not POST /filter if a matching filter already exists", async function() {
httpLookups = []; httpLookups = [];
httpLookups.push(PUSH_RULES_RESPONSE); httpLookups.push(PUSH_RULES_RESPONSE);
httpLookups.push(SYNC_RESPONSE); httpLookups.push(SYNC_RESPONSE);
@@ -191,15 +194,19 @@ describe("MatrixClient", function() {
const filter = new sdk.Filter(0, filterId); const filter = new sdk.Filter(0, filterId);
filter.setDefinition({"room": {"timeline": {"limit": 8}}}); filter.setDefinition({"room": {"timeline": {"limit": 8}}});
store.getFilter.andReturn(filter); store.getFilter.andReturn(filter);
client.startClient(); const syncPromise = new Promise((resolve, reject) => {
client.on("sync", function syncListener(state) {
client.on("sync", function syncListener(state) { if (state === "SYNCING") {
if (state === "SYNCING") { expect(httpLookups.length).toEqual(0);
expect(httpLookups.length).toEqual(0); client.removeListener("sync", syncListener);
client.removeListener("sync", syncListener); resolve();
done(); } else if (state === "ERROR") {
} reject(new Error("sync error"));
}
});
}); });
await client.startClient();
await syncPromise;
}); });
describe("getSyncState", function() { describe("getSyncState", function() {
@@ -207,15 +214,18 @@ describe("MatrixClient", function() {
expect(client.getSyncState()).toBe(null); expect(client.getSyncState()).toBe(null);
}); });
it("should return the same sync state as emitted sync events", function(done) { it("should return the same sync state as emitted sync events", async function() {
client.on("sync", function syncListener(state) { const syncingPromise = new Promise((resolve) => {
expect(state).toEqual(client.getSyncState()); client.on("sync", function syncListener(state) {
if (state === "SYNCING") { expect(state).toEqual(client.getSyncState());
client.removeListener("sync", syncListener); if (state === "SYNCING") {
done(); client.removeListener("sync", syncListener);
} resolve();
}
});
}); });
client.startClient(); await client.startClient();
await syncingPromise;
}); });
}); });
@@ -258,8 +268,8 @@ describe("MatrixClient", function() {
}); });
describe("retryImmediately", function() { describe("retryImmediately", function() {
it("should return false if there is no request waiting", function() { it("should return false if there is no request waiting", async function() {
client.startClient(); await client.startClient();
expect(client.retryImmediately()).toBe(false); expect(client.retryImmediately()).toBe(false);
}); });

View File

@@ -1402,13 +1402,25 @@ describe("Room", function() {
it("should return synced membership if membership isn't available yet", it("should return synced membership if membership isn't available yet",
function() { function() {
const room = new Room(roomId, null, userA); const room = new Room(roomId, null, userA);
room.setSyncedMembership("invite"); room.updateMyMembership("invite");
expect(room.getMyMembership()).toEqual("invite"); expect(room.getMyMembership()).toEqual("invite");
room.addLiveEvents([utils.mkMembership({ });
user: userA, mship: "join", it("should emit a Room.myMembership event on a change",
room: roomId, event: true, function() {
})]); const room = new Room(roomId, null, userA);
const events = [];
room.on("Room.myMembership", (_room, membership, oldMembership) => {
events.push({membership, oldMembership});
});
room.updateMyMembership("invite");
expect(room.getMyMembership()).toEqual("invite");
expect(events[0]).toEqual({membership: "invite", oldMembership: null});
events.splice(0); //clear
room.updateMyMembership("invite");
expect(events.length).toEqual(0);
room.updateMyMembership("join");
expect(room.getMyMembership()).toEqual("join"); expect(room.getMyMembership()).toEqual("join");
expect(events[0]).toEqual({membership: "join", oldMembership: "invite"});
}); });
}); });
@@ -1439,11 +1451,11 @@ describe("Room", function() {
it("should return false if synced membership not join", it("should return false if synced membership not join",
function() { function() {
const room = new Room(roomId, null, userA); const room = new Room(roomId, null, userA);
room.setSyncedMembership("invite"); room.updateMyMembership("invite");
expect(room.maySendMessage()).toEqual(false); expect(room.maySendMessage()).toEqual(false);
room.setSyncedMembership("leave"); room.updateMyMembership("leave");
expect(room.maySendMessage()).toEqual(false); expect(room.maySendMessage()).toEqual(false);
room.setSyncedMembership("join"); room.updateMyMembership("join");
expect(room.maySendMessage()).toEqual(true); expect(room.maySendMessage()).toEqual(true);
}); });
}); });

View File

@@ -45,7 +45,15 @@ const olmlib = require("./crypto/olmlib");
import ReEmitter from './ReEmitter'; import ReEmitter from './ReEmitter';
import RoomList from './crypto/RoomList'; import RoomList from './crypto/RoomList';
import {InvalidStoreError} from './errors';
import Crypto from './crypto';
import { isCryptoAvailable } from './crypto';
import { encodeRecoveryKey, decodeRecoveryKey } from './crypto/recoverykey';
// Disable warnings for now: we use deprecated bluebird functions
// and need to migrate, but they spam the console with warnings.
Promise.config({warnings: false});
const LAZY_LOADING_MESSAGES_FILTER = { const LAZY_LOADING_MESSAGES_FILTER = {
lazy_load_members: true, lazy_load_members: true,
@@ -59,14 +67,7 @@ const LAZY_LOADING_SYNC_FILTER = {
const SCROLLBACK_DELAY_MS = 3000; const SCROLLBACK_DELAY_MS = 3000;
let CRYPTO_ENABLED = false; const CRYPTO_ENABLED = isCryptoAvailable();
try {
var Crypto = require("./crypto");
CRYPTO_ENABLED = true;
} catch (e) {
console.warn("Unable to load crypto module: crypto will be disabled: " + e);
}
function keysFromRecoverySession(sessions, decryptionKey, roomId) { function keysFromRecoverySession(sessions, decryptionKey, roomId) {
const keys = []; const keys = [];
@@ -164,6 +165,8 @@ function MatrixClient(opts) {
MatrixBaseApis.call(this, opts); MatrixBaseApis.call(this, opts);
this.olmVersion = null; // Populated after initCrypto is done
this.reEmitter = new ReEmitter(this); this.reEmitter = new ReEmitter(this);
this.store = opts.store || new StubStore(); this.store = opts.store || new StubStore();
@@ -216,10 +219,6 @@ function MatrixClient(opts) {
this._forceTURN = opts.forceTURN || false; this._forceTURN = opts.forceTURN || false;
if (CRYPTO_ENABLED) {
this.olmVersion = Crypto.getOlmVersion();
}
// List of which rooms have encryption enabled: separate from crypto because // List of which rooms have encryption enabled: separate from crypto because
// we still want to know which rooms are encrypted even if crypto is disabled: // we still want to know which rooms are encrypted even if crypto is disabled:
// we don't want to start sending unencrypted events to them. // we don't want to start sending unencrypted events to them.
@@ -409,6 +408,13 @@ MatrixClient.prototype.setNotifTimelineSet = function(notifTimelineSet) {
* successfully initialised. * successfully initialised.
*/ */
MatrixClient.prototype.initCrypto = async function() { MatrixClient.prototype.initCrypto = async function() {
if (!isCryptoAvailable()) {
throw new Error(
`End-to-end encryption not supported in this js-sdk build: did ` +
`you remember to load the olm library?`,
);
}
if (this._crypto) { if (this._crypto) {
console.warn("Attempt to re-initialise e2e encryption on MatrixClient"); console.warn("Attempt to re-initialise e2e encryption on MatrixClient");
return; return;
@@ -426,13 +432,6 @@ MatrixClient.prototype.initCrypto = async function() {
// initialise the list of encrypted rooms (whether or not crypto is enabled) // initialise the list of encrypted rooms (whether or not crypto is enabled)
await this._roomList.init(); await this._roomList.init();
if (!CRYPTO_ENABLED) {
throw new Error(
`End-to-end encryption not supported in this js-sdk build: did ` +
`you remember to load the olm library?`,
);
}
const userId = this.getUserId(); const userId = this.getUserId();
if (userId === null) { if (userId === null) {
throw new Error( throw new Error(
@@ -464,6 +463,9 @@ MatrixClient.prototype.initCrypto = async function() {
await crypto.init(); await crypto.init();
this.olmVersion = Crypto.getOlmVersion();
// if crypto initialisation was successful, tell it to attach its event // if crypto initialisation was successful, tell it to attach its event
// handlers. // handlers.
crypto.registerEventHandlers(this); crypto.registerEventHandlers(this);
@@ -887,9 +889,7 @@ MatrixClient.prototype.prepareKeyBackupVersion = function() {
auth_data: { auth_data: {
public_key: publicKey, public_key: publicKey,
}, },
// FIXME: pickle isn't the right thing to use, but we don't have recovery_key: encodeRecoveryKey(decryption.get_private_key()),
// anything else yet, so use it for now
recovery_key: decryption.pickle("secret_key"),
}; };
} finally { } finally {
decryption.free(); decryption.free();
@@ -996,26 +996,17 @@ MatrixClient.prototype.backupAllGroupSessions = function(version) {
return this._crypto.backupAllGroupSessions(version); return this._crypto.backupAllGroupSessions(version);
}; };
MatrixClient.prototype.isValidRecoveryKey = function(decryptionKey) { MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
}
const decryption = new global.Olm.PkDecryption();
try { try {
// FIXME: see the FIXME in createKeyBackupVersion decodeRecoveryKey(recoveryKey);
decryption.unpickle("secret_key", decryptionKey);
return true; return true;
} catch (e) { } catch (e) {
console.log(e);
return false; return false;
} finally {
decryption.free();
} }
}; };
MatrixClient.prototype.restoreKeyBackups = function( MatrixClient.prototype.restoreKeyBackups = function(
decryptionKey, targetRoomId, targetSessionId, version, recoveryKey, targetRoomId, targetSessionId, version,
) { ) {
if (this._crypto === null) { if (this._crypto === null) {
throw new Error("End-to-end encryption disabled"); throw new Error("End-to-end encryption disabled");
@@ -1026,9 +1017,10 @@ MatrixClient.prototype.restoreKeyBackups = function(
const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version); const path = this._makeKeyBackupPath(targetRoomId, targetSessionId, version);
// FIXME: see the FIXME in createKeyBackupVersion // FIXME: see the FIXME in createKeyBackupVersion
const privkey = decodeRecoveryKey(recoveryKey);
const decryption = new global.Olm.PkDecryption(); const decryption = new global.Olm.PkDecryption();
try { try {
decryption.unpickle("secret_key", decryptionKey); decryption.init_with_private_key(privkey);
} catch(e) { } catch(e) {
decryption.free(); decryption.free();
throw e; throw e;
@@ -1042,7 +1034,9 @@ MatrixClient.prototype.restoreKeyBackups = function(
if (!roomData.sessions) continue; if (!roomData.sessions) continue;
totalKeyCount += Object.keys(roomData.sessions).length; totalKeyCount += Object.keys(roomData.sessions).length;
const roomKeys = keysFromRecoverySession(roomData.sessions, decryption, roomId, roomKeys); const roomKeys = keysFromRecoverySession(
roomData.sessions, decryption, roomId, roomKeys,
);
for (const k of roomKeys) { for (const k of roomKeys) {
k.room_id = roomId; k.room_id = roomId;
keys.push(k); keys.push(k);
@@ -2439,7 +2433,8 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) {
self.getEventMapper())); self.getEventMapper()));
timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end; timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end;
} else { } else {
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(res.state); const stateEvents = utils.map(res.state, self.getEventMapper());
timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(stateEvents);
} }
timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start); timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start);
@@ -3441,6 +3436,9 @@ MatrixClient.prototype.startClient = async function(opts) {
// shallow-copy the opts dict before modifying and storing it // shallow-copy the opts dict before modifying and storing it
opts = Object.assign({}, opts); opts = Object.assign({}, opts);
if (opts.lazyLoadMembers && this.isGuest()) {
opts.lazyLoadMembers = false;
}
if (opts.lazyLoadMembers) { if (opts.lazyLoadMembers) {
const supported = await this.doesServerSupportLazyLoading(); const supported = await this.doesServerSupportLazyLoading();
if (supported) { if (supported) {
@@ -3451,7 +3449,12 @@ MatrixClient.prototype.startClient = async function(opts) {
opts.lazyLoadMembers = false; opts.lazyLoadMembers = false;
} }
} }
// need to vape the store when enabling LL and wasn't enabled before
const shouldClear = await this._wasLazyLoadingToggled(opts.lazyLoadMembers);
if (shouldClear) {
const reason = InvalidStoreError.TOGGLED_LAZY_LOADING;
throw new InvalidStoreError(reason, !!opts.lazyLoadMembers);
}
if (opts.lazyLoadMembers && this._crypto) { if (opts.lazyLoadMembers && this._crypto) {
this._crypto.enableLazyLoading(); this._crypto.enableLazyLoading();
} }
@@ -3464,11 +3467,50 @@ MatrixClient.prototype.startClient = async function(opts) {
return this._canResetTimelineCallback(roomId); return this._canResetTimelineCallback(roomId);
}; };
this._clientOpts = opts; this._clientOpts = opts;
await this._storeClientOptions(this._clientOpts);
this._syncApi = new SyncApi(this, opts); this._syncApi = new SyncApi(this, opts);
this._syncApi.sync(); this._syncApi.sync();
}; };
/**
* Is the lazy loading option different than in previous session?
* @param {bool} lazyLoadMembers current options for lazy loading
* @return {bool} whether or not the option has changed compared to the previous session */
MatrixClient.prototype._wasLazyLoadingToggled = async function(lazyLoadMembers) {
lazyLoadMembers = !!lazyLoadMembers;
// assume it was turned off before
// if we don't know any better
let lazyLoadMembersBefore = false;
const isStoreNewlyCreated = await this.store.isNewlyCreated();
if (!isStoreNewlyCreated) {
const prevClientOptions = await this.store.getClientOptions();
if (prevClientOptions) {
lazyLoadMembersBefore = !!prevClientOptions.lazyLoadMembers;
}
return lazyLoadMembersBefore !== lazyLoadMembers;
}
return false;
};
/**
* store client options with boolean/string/numeric values
* to know in the next session what flags the sync data was
* created with (e.g. lazy loading)
* @param {object} opts the complete set of client options
* @return {Promise} for store operation */
MatrixClient.prototype._storeClientOptions = function(opts) {
const primTypes = ["boolean", "string", "number"];
const serializableOpts = Object.entries(opts)
.filter(([key, value]) => {
return primTypes.includes(typeof value);
})
.reduce((obj, [key, value]) => {
obj[key] = value;
return obj;
}, {});
return this.store.storeClientOptions(serializableOpts);
};
/** /**
* High level helper method to stop the client from polling and allow a * High level helper method to stop the client from polling and allow a
* clean shutdown. * clean shutdown.

View File

@@ -17,17 +17,6 @@ limitations under the License.
import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
/**
* olm.js wrapper
*
* @module crypto/OlmDevice
*/
const Olm = global.Olm;
if (!Olm) {
throw new Error("global.Olm is not defined");
}
// The maximum size of an event is 65K, and we base64 the content, so this is a // The maximum size of an event is 65K, and we base64 the content, so this is a
// reasonable approximation to the biggest plaintext we can encrypt. // reasonable approximation to the biggest plaintext we can encrypt.
const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4; const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4;
@@ -138,7 +127,7 @@ OlmDevice.prototype.init = async function() {
await this._migrateFromSessionStore(); await this._migrateFromSessionStore();
let e2eKeys; let e2eKeys;
const account = new Olm.Account(); const account = new global.Olm.Account();
try { try {
await _initialiseAccount( await _initialiseAccount(
this._sessionStore, this._cryptoStore, this._pickleKey, account, this._sessionStore, this._cryptoStore, this._pickleKey, account,
@@ -172,7 +161,7 @@ async function _initialiseAccount(sessionStore, cryptoStore, pickleKey, account)
* @return {array} The version of Olm. * @return {array} The version of Olm.
*/ */
OlmDevice.getOlmVersion = function() { OlmDevice.getOlmVersion = function() {
return Olm.get_library_version(); return global.Olm.get_library_version();
}; };
OlmDevice.prototype._migrateFromSessionStore = async function() { OlmDevice.prototype._migrateFromSessionStore = async function() {
@@ -279,7 +268,7 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
*/ */
OlmDevice.prototype._getAccount = function(txn, func) { OlmDevice.prototype._getAccount = function(txn, func) {
this._cryptoStore.getAccount(txn, (pickledAccount) => { this._cryptoStore.getAccount(txn, (pickledAccount) => {
const account = new Olm.Account(); const account = new global.Olm.Account();
try { try {
account.unpickle(this._pickleKey, pickledAccount); account.unpickle(this._pickleKey, pickledAccount);
func(account); func(account);
@@ -332,7 +321,7 @@ OlmDevice.prototype._getSession = function(deviceKey, sessionId, txn, func) {
* @private * @private
*/ */
OlmDevice.prototype._unpickleSession = function(pickledSession, func) { OlmDevice.prototype._unpickleSession = function(pickledSession, func) {
const session = new Olm.Session(); const session = new global.Olm.Session();
try { try {
session.unpickle(this._pickleKey, pickledSession); session.unpickle(this._pickleKey, pickledSession);
func(session); func(session);
@@ -365,7 +354,7 @@ OlmDevice.prototype._saveSession = function(deviceKey, session, txn) {
* @private * @private
*/ */
OlmDevice.prototype._getUtility = function(func) { OlmDevice.prototype._getUtility = function(func) {
const utility = new Olm.Utility(); const utility = new global.Olm.Utility();
try { try {
return func(utility); return func(utility);
} finally { } finally {
@@ -477,7 +466,7 @@ OlmDevice.prototype.createOutboundSession = async function(
], ],
(txn) => { (txn) => {
this._getAccount(txn, (account) => { this._getAccount(txn, (account) => {
const session = new Olm.Session(); const session = new global.Olm.Session();
try { try {
session.create_outbound(account, theirIdentityKey, theirOneTimeKey); session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
newSessionId = session.session_id(); newSessionId = session.session_id();
@@ -521,7 +510,7 @@ OlmDevice.prototype.createInboundSession = async function(
], ],
(txn) => { (txn) => {
this._getAccount(txn, (account) => { this._getAccount(txn, (account) => {
const session = new Olm.Session(); const session = new global.Olm.Session();
try { try {
session.create_inbound_from( session.create_inbound_from(
account, theirDeviceIdentityKey, ciphertext, account, theirDeviceIdentityKey, ciphertext,
@@ -739,7 +728,7 @@ OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
throw new Error("Unknown outbound group session " + sessionId); throw new Error("Unknown outbound group session " + sessionId);
} }
const session = new Olm.OutboundGroupSession(); const session = new global.Olm.OutboundGroupSession();
try { try {
session.unpickle(this._pickleKey, pickled); session.unpickle(this._pickleKey, pickled);
return func(session); return func(session);
@@ -755,7 +744,7 @@ OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
* @return {string} sessionId for the outbound session. * @return {string} sessionId for the outbound session.
*/ */
OlmDevice.prototype.createOutboundGroupSession = function() { OlmDevice.prototype.createOutboundGroupSession = function() {
const session = new Olm.OutboundGroupSession(); const session = new global.Olm.OutboundGroupSession();
try { try {
session.create(); session.create();
this._saveOutboundGroupSession(session); this._saveOutboundGroupSession(session);
@@ -827,7 +816,7 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
* @return {*} result of func * @return {*} result of func
*/ */
OlmDevice.prototype._unpickleInboundGroupSession = function(sessionData, func) { OlmDevice.prototype._unpickleInboundGroupSession = function(sessionData, func) {
const session = new Olm.InboundGroupSession(); const session = new global.Olm.InboundGroupSession();
try { try {
session.unpickle(this._pickleKey, sessionData.session); session.unpickle(this._pickleKey, sessionData.session);
return func(session); return func(session);
@@ -908,7 +897,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
} }
// new session. // new session.
const session = new Olm.InboundGroupSession(); const session = new global.Olm.InboundGroupSession();
try { try {
if (exportFormat) { if (exportFormat) {
session.import_session(sessionKey); session.import_session(sessionKey);

View File

@@ -36,9 +36,8 @@ const DeviceList = require('./DeviceList').default;
import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager'; import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store'; import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
const Olm = global.Olm; export function isCryptoAvailable() {
if (!Olm) { return Boolean(global.Olm);
throw new Error("global.Olm is not defined");
} }
/** /**
@@ -67,7 +66,7 @@ if (!Olm) {
* *
* @param {RoomList} roomList An initialised RoomList object * @param {RoomList} roomList An initialised RoomList object
*/ */
function Crypto(baseApis, sessionStore, userId, deviceId, export default function Crypto(baseApis, sessionStore, userId, deviceId,
clientStore, cryptoStore, roomList) { clientStore, cryptoStore, roomList) {
this._baseApis = baseApis; this._baseApis = baseApis;
this._sessionStore = sessionStore; this._sessionStore = sessionStore;
@@ -137,6 +136,10 @@ utils.inherits(Crypto, EventEmitter);
* Returns a promise which resolves once the crypto module is ready for use. * Returns a promise which resolves once the crypto module is ready for use.
*/ */
Crypto.prototype.init = async function() { Crypto.prototype.init = async function() {
// Olm is just an object with a .then, not a fully-fledged promise, so
// pass it into bluebird to make it a proper promise.
await global.Olm.init();
const sessionStoreHasAccount = Boolean(this._sessionStore.getEndToEndAccount()); const sessionStoreHasAccount = Boolean(this._sessionStore.getEndToEndAccount());
let cryptoStoreHasAccount; let cryptoStoreHasAccount;
await this._cryptoStore.doTxn( await this._cryptoStore.doTxn(
@@ -1747,6 +1750,3 @@ class IncomingRoomKeyRequestCancellation {
* @event module:client~MatrixClient#"crypto.warning" * @event module:client~MatrixClient#"crypto.warning"
* @param {string} type One of the strings listed above * @param {string} type One of the strings listed above
*/ */
/** */
module.exports = Crypto;

67
src/crypto/recoverykey.js Normal file
View File

@@ -0,0 +1,67 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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 bs58 from 'bs58';
// picked arbitrarily but to try & avoid clashing with any bitcoin ones
// (also base58 encoded, albeit with a lot of hashing)
const OLM_RECOVERY_KEY_PREFIX = [0x8B, 0x01];
export function encodeRecoveryKey(key) {
const buf = new Uint8Array(OLM_RECOVERY_KEY_PREFIX.length + key.length + 1);
buf.set(OLM_RECOVERY_KEY_PREFIX, 0);
buf.set(key, OLM_RECOVERY_KEY_PREFIX.length);
let parity = 0;
for (let i = 0; i < buf.length - 1; ++i) {
parity ^= buf[i];
}
buf[buf.length - 1] = parity;
const base58key = bs58.encode(buf);
return base58key.match(/.{1,4}/g).join(" ");
}
export function decodeRecoveryKey(recoverykey) {
const result = bs58.decode(recoverykey.replace(/ /g, ''));
let parity = 0;
for (const b of result) {
parity ^= b;
}
if (parity !== 0) {
throw new Error("Incorrect parity");
}
for (let i = 0; i < OLM_RECOVERY_KEY_PREFIX.length; ++i) {
if (result[i] !== OLM_RECOVERY_KEY_PREFIX[i]) {
throw new Error("Incorrect prefix");
}
}
if (
result.length !==
OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH + 1
) {
throw new Error("Incorrect length");
}
return result.slice(
OLM_RECOVERY_KEY_PREFIX.length,
OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH,
);
}

25
src/errors.js Normal file
View File

@@ -0,0 +1,25 @@
// can't just do InvalidStoreError extends Error
// because of http://babeljs.io/docs/usage/caveats/#classes
function InvalidStoreError(reason, value) {
const message = `Store is invalid because ${reason}, ` +
`please delete all data and retry`;
const instance = Reflect.construct(Error, [message]);
Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this));
instance.reason = reason;
instance.value = value;
return instance;
}
InvalidStoreError.TOGGLED_LAZY_LOADING = "TOGGLED_LAZY_LOADING";
InvalidStoreError.prototype = Object.create(Error.prototype, {
constructor: {
value: Error,
enumerable: false,
writable: true,
configurable: true,
},
});
Reflect.setPrototypeOf(InvalidStoreError, Error);
module.exports.InvalidStoreError = InvalidStoreError;

View File

@@ -34,6 +34,8 @@ module.exports.SyncAccumulator = require("./sync-accumulator");
module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi; module.exports.MatrixHttpApi = require("./http-api").MatrixHttpApi;
/** The {@link module:http-api.MatrixError|MatrixError} class. */ /** The {@link module:http-api.MatrixError|MatrixError} class. */
module.exports.MatrixError = require("./http-api").MatrixError; module.exports.MatrixError = require("./http-api").MatrixError;
/** The {@link module:errors.InvalidStoreError|InvalidStoreError} class. */
module.exports.InvalidStoreError = require("./errors").InvalidStoreError;
/** The {@link module:client.MatrixClient|MatrixClient} class. */ /** The {@link module:client.MatrixClient|MatrixClient} class. */
module.exports.MatrixClient = require("./client").MatrixClient; module.exports.MatrixClient = require("./client").MatrixClient;
/** The {@link module:models/room|Room} class. */ /** The {@link module:models/room|Room} class. */

View File

@@ -180,7 +180,7 @@ RoomState.prototype.getSentinelMember = function(userId) {
sentinel = new RoomMember(this.roomId, userId); sentinel = new RoomMember(this.roomId, userId);
const member = this.members[userId]; const member = this.members[userId];
if (member) { if (member) {
sentinel.setMembershipEvent(member.events.member); sentinel.setMembershipEvent(member.events.member, this);
} }
this._sentinels[userId] = sentinel; this._sentinels[userId] = sentinel;
} }
@@ -501,7 +501,7 @@ RoomState.prototype._setOutOfBandMember = function(stateEvent) {
} }
const member = this._getOrCreateMember(userId, stateEvent); const member = this._getOrCreateMember(userId, stateEvent);
member.setMembershipEvent(stateEvent); member.setMembershipEvent(stateEvent, this);
// needed to know which members need to be stored seperately // needed to know which members need to be stored seperately
// as the are not part of the sync accumulator // as the are not part of the sync accumulator
// this is cleared by setMembershipEvent so when it's updated through /sync // this is cleared by setMembershipEvent so when it's updated through /sync

View File

@@ -178,7 +178,7 @@ function Room(roomId, client, myUserId, opts) {
// read by megolm; boolean value - null indicates "use global value" // read by megolm; boolean value - null indicates "use global value"
this._blacklistUnverifiedDevices = null; this._blacklistUnverifiedDevices = null;
this._syncedMembership = null; this._selfMembership = null;
this._summaryHeroes = null; this._summaryHeroes = null;
// awaited by getEncryptionTargetMembers while room members are loading // awaited by getEncryptionTargetMembers while room members are loading
@@ -198,7 +198,10 @@ utils.inherits(Room, EventEmitter);
*/ */
Room.prototype.getVersion = function() { Room.prototype.getVersion = function() {
const createEvent = this.currentState.getStateEvents("m.room.create", ""); const createEvent = this.currentState.getStateEvents("m.room.create", "");
if (!createEvent) return null; if (!createEvent) {
console.warn("Room " + this.room_id + " does not have an m.room.create event");
return '1';
}
const ver = createEvent.getContent()['room_version']; const ver = createEvent.getContent()['room_version'];
if (ver === undefined) return '1'; if (ver === undefined) return '1';
return ver; return ver;
@@ -257,13 +260,7 @@ Room.prototype.getLiveTimeline = function() {
* @return {string} the membership type (join | leave | invite) for the logged in user * @return {string} the membership type (join | leave | invite) for the logged in user
*/ */
Room.prototype.getMyMembership = function() { Room.prototype.getMyMembership = function() {
if (this.myUserId) { return this._selfMembership;
const me = this.getMember(this.myUserId);
if (me) {
return me.membership;
}
}
return this._syncedMembership;
}; };
/** /**
@@ -278,7 +275,7 @@ Room.prototype.getDMInviter = function() {
return me.getDMInviter(); return me.getDMInviter();
} }
} }
if (this._syncedMembership === "invite") { if (this._selfMembership === "invite") {
// fall back to summary information // fall back to summary information
const memberCount = this.getInvitedAndJoinedMemberCount(); const memberCount = this.getInvitedAndJoinedMemberCount();
if (memberCount == 2 && this._summaryHeroes.length) { if (memberCount == 2 && this._summaryHeroes.length) {
@@ -362,8 +359,15 @@ Room.prototype.getAvatarFallbackMember = function() {
* Sets the membership this room was received as during sync * Sets the membership this room was received as during sync
* @param {string} membership join | leave | invite * @param {string} membership join | leave | invite
*/ */
Room.prototype.setSyncedMembership = function(membership) { Room.prototype.updateMyMembership = function(membership) {
this._syncedMembership = membership; const prevMembership = this._selfMembership;
this._selfMembership = membership;
if (prevMembership !== membership) {
if (membership === "leave") {
this._cleanupAfterLeaving();
}
this.emit("Room.myMembership", this, membership, prevMembership);
}
}; };
Room.prototype._loadMembersFromServer = async function() { Room.prototype._loadMembersFromServer = async function() {
@@ -470,7 +474,7 @@ Room.prototype.clearLoadedMembersIfNeeded = async function() {
* called when sync receives this room in the leave section * called when sync receives this room in the leave section
* to do cleanup after leaving a room. Possibly called multiple times. * to do cleanup after leaving a room. Possibly called multiple times.
*/ */
Room.prototype.onLeft = function() { Room.prototype._cleanupAfterLeaving = function() {
this.clearLoadedMembersIfNeeded().catch((err) => { this.clearLoadedMembersIfNeeded().catch((err) => {
console.error(`error after clearing loaded members from ` + console.error(`error after clearing loaded members from ` +
`room ${this.roomId} after leaving`); `room ${this.roomId} after leaving`);

View File

@@ -19,7 +19,7 @@ import Promise from 'bluebird';
import SyncAccumulator from "../sync-accumulator"; import SyncAccumulator from "../sync-accumulator";
import utils from "../utils"; import utils from "../utils";
const VERSION = 2; const VERSION = 3;
function createDatabase(db) { function createDatabase(db) {
// Make user store, clobber based on user ID. (userId property of User objects) // Make user store, clobber based on user ID. (userId property of User objects)
@@ -41,6 +41,12 @@ function upgradeSchemaV2(db) {
oobMembersStore.createIndex("room", "room_id"); oobMembersStore.createIndex("room", "room_id");
} }
function upgradeSchemaV3(db) {
db.createObjectStore("client_options",
{ keyPath: ["clobber"]});
}
/** /**
* Helper method to collect results from a Cursor and promiseify it. * Helper method to collect results from a Cursor and promiseify it.
* @param {ObjectStore|Index} store The store to perform openCursor on. * @param {ObjectStore|Index} store The store to perform openCursor on.
@@ -77,7 +83,7 @@ function txnAsPromise(txn) {
resolve(event); resolve(event);
}; };
txn.onerror = function(event) { txn.onerror = function(event) {
reject(event); reject(event.target.error);
}; };
}); });
} }
@@ -88,7 +94,7 @@ function reqAsEventPromise(req) {
resolve(event); resolve(event);
}; };
req.onerror = function(event) { req.onerror = function(event) {
reject(event); reject(event.target.error);
}; };
}); });
} }
@@ -123,6 +129,7 @@ const LocalIndexedDBStoreBackend = function LocalIndexedDBStoreBackend(
this.db = null; this.db = null;
this._disconnected = true; this._disconnected = true;
this._syncAccumulator = new SyncAccumulator(); this._syncAccumulator = new SyncAccumulator();
this._isNewlyCreated = false;
}; };
@@ -153,11 +160,15 @@ LocalIndexedDBStoreBackend.prototype = {
`LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`, `LocalIndexedDBStoreBackend.connect: upgrading from ${oldVersion}`,
); );
if (oldVersion < 1) { // The database did not previously exist. if (oldVersion < 1) { // The database did not previously exist.
this._isNewlyCreated = true;
createDatabase(db); createDatabase(db);
} }
if (oldVersion < 2) { if (oldVersion < 2) {
upgradeSchemaV2(db); upgradeSchemaV2(db);
} }
if (oldVersion < 3) {
upgradeSchemaV3(db);
}
// Expand as needed. // Expand as needed.
}; };
@@ -185,6 +196,10 @@ LocalIndexedDBStoreBackend.prototype = {
return this._init(); return this._init();
}); });
}, },
/** @return {bool} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
return Promise.resolve(this._isNewlyCreated);
},
/** /**
* Having connected, load initial data from the database and prepare for use * Having connected, load initial data from the database and prepare for use
@@ -529,6 +544,28 @@ LocalIndexedDBStoreBackend.prototype = {
}); });
}); });
}, },
getClientOptions: function() {
return Promise.resolve().then(() => {
const txn = this.db.transaction(["client_options"], "readonly");
const store = txn.objectStore("client_options");
return selectQuery(store, undefined, (cursor) => {
if (cursor.value && cursor.value && cursor.value.options) {
return cursor.value.options;
}
}).then((results) => results[0]);
});
},
storeClientOptions: async function(options) {
const txn = this.db.transaction(["client_options"], "readwrite");
const store = txn.objectStore("client_options");
store.put({
clobber: "-", // constant key so will always clobber
options: options,
}); // put == UPSERT
await txnAsPromise(txn);
},
}; };
export default LocalIndexedDBStoreBackend; export default LocalIndexedDBStoreBackend;

View File

@@ -65,7 +65,10 @@ RemoteIndexedDBStoreBackend.prototype = {
clearDatabase: function() { clearDatabase: function() {
return this._ensureStarted().then(() => this._doCmd('clearDatabase')); return this._ensureStarted().then(() => this._doCmd('clearDatabase'));
}, },
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
return this._doCmd('isNewlyCreated');
},
/** /**
* @return {Promise} Resolves with a sync response to restore the * @return {Promise} Resolves with a sync response to restore the
* client state to where it was at the last save, or null if there * client state to where it was at the last save, or null if there
@@ -114,6 +117,14 @@ RemoteIndexedDBStoreBackend.prototype = {
return this._doCmd('clearOutOfBandMembers', [roomId]); return this._doCmd('clearOutOfBandMembers', [roomId]);
}, },
getClientOptions: function() {
return this._doCmd('getClientOptions');
},
storeClientOptions: function(options) {
return this._doCmd('storeClientOptions', [options]);
},
/** /**
* Load all user presence events from the database. This is not cached. * Load all user presence events from the database. This is not cached.
* @return {Promise<Object[]>} A list of presence events in their raw form. * @return {Promise<Object[]>} A list of presence events in their raw form.
@@ -173,7 +184,9 @@ RemoteIndexedDBStoreBackend.prototype = {
if (msg.command == 'cmd_success') { if (msg.command == 'cmd_success') {
def.resolve(msg.result); def.resolve(msg.result);
} else { } else {
def.reject(msg.error); const error = new Error(msg.error.message);
error.name = msg.error.name;
def.reject(error);
} }
} else { } else {
console.warn("Unrecognised message from worker: " + msg); console.warn("Unrecognised message from worker: " + msg);

View File

@@ -67,6 +67,9 @@ class IndexedDBStoreWorker {
case 'connect': case 'connect':
prom = this.backend.connect(); prom = this.backend.connect();
break; break;
case 'isNewlyCreated':
prom = this.backend.isNewlyCreated();
break;
case 'clearDatabase': case 'clearDatabase':
prom = this.backend.clearDatabase().then((result) => { prom = this.backend.clearDatabase().then((result) => {
// This returns special classes which can't be cloned // This returns special classes which can't be cloned
@@ -101,10 +104,16 @@ class IndexedDBStoreWorker {
case 'setOutOfBandMembers': case 'setOutOfBandMembers':
prom = this.backend.setOutOfBandMembers(msg.args[0], msg.args[1]); prom = this.backend.setOutOfBandMembers(msg.args[0], msg.args[1]);
break; break;
case 'getClientOptions':
prom = this.backend.getClientOptions();
break;
case 'storeClientOptions':
prom = this.backend.storeClientOptions(msg.args[0]);
break;
} }
if (prom === undefined) { if (prom === undefined) {
postMessage({ this.postMessage({
command: 'cmd_fail', command: 'cmd_fail',
seq: msg.seq, seq: msg.seq,
// Can't be an Error because they're not structured cloneable // Can't be an Error because they're not structured cloneable
@@ -126,7 +135,10 @@ class IndexedDBStoreWorker {
command: 'cmd_fail', command: 'cmd_fail',
seq: msg.seq, seq: msg.seq,
// Just send a string because Error objects aren't cloneable // Just send a string because Error objects aren't cloneable
error: "Error running command", error: {
message: err.message,
name: err.name,
},
}); });
}); });
} }

View File

@@ -146,6 +146,11 @@ IndexedDBStore.prototype.getSavedSync = function() {
return this.backend.getSavedSync(); return this.backend.getSavedSync();
}; };
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
IndexedDBStore.prototype.isNewlyCreated = function() {
return this.backend.isNewlyCreated();
};
/** /**
* @return {Promise} If there is a saved sync, the nextBatch token * @return {Promise} If there is a saved sync, the nextBatch token
* for this sync, otherwise null. * for this sync, otherwise null.
@@ -246,4 +251,12 @@ IndexedDBStore.prototype.clearOutOfBandMembers = function(roomId) {
return this.backend.clearOutOfBandMembers(roomId); return this.backend.clearOutOfBandMembers(roomId);
}; };
IndexedDBStore.prototype.getClientOptions = function() {
return this.backend.getClientOptions();
};
IndexedDBStore.prototype.storeClientOptions = function(options) {
return this.backend.storeClientOptions(options);
};
module.exports.IndexedDBStore = IndexedDBStore; module.exports.IndexedDBStore = IndexedDBStore;

View File

@@ -55,6 +55,7 @@ module.exports.MatrixInMemoryStore = function MatrixInMemoryStore(opts) {
this._oobMembers = { this._oobMembers = {
// roomId: [member events] // roomId: [member events]
}; };
this._clientOptions = {};
}; };
module.exports.MatrixInMemoryStore.prototype = { module.exports.MatrixInMemoryStore.prototype = {
@@ -67,6 +68,10 @@ module.exports.MatrixInMemoryStore.prototype = {
return this.syncToken; return this.syncToken;
}, },
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
return Promise.resolve(true);
},
/** /**
* Set the token to stream from. * Set the token to stream from.
@@ -402,4 +407,13 @@ module.exports.MatrixInMemoryStore.prototype = {
this._oobMembers[roomId] = membershipEvents; this._oobMembers[roomId] = membershipEvents;
return Promise.resolve(); return Promise.resolve();
}, },
getClientOptions: function() {
return Promise.resolve(this._clientOptions);
},
storeClientOptions: function(options) {
this._clientOptions = Object.assign({}, options);
return Promise.resolve();
},
}; };

View File

@@ -32,6 +32,11 @@ function StubStore() {
StubStore.prototype = { StubStore.prototype = {
/** @return {Promise<bool>} whether or not the database was newly created in this session. */
isNewlyCreated: function() {
return Promise.resolve(true);
},
/** /**
* Get the sync token. * Get the sync token.
* @return {string} * @return {string}
@@ -276,6 +281,14 @@ StubStore.prototype = {
clearOutOfBandMembers: function() { clearOutOfBandMembers: function() {
return Promise.resolve(); return Promise.resolve();
}, },
getClientOptions: function() {
return Promise.resolve();
},
storeClientOptions: function() {
return Promise.resolve();
},
}; };
/** Stub Store class. */ /** Stub Store class. */

View File

@@ -123,6 +123,7 @@ SyncApi.prototype.createRoom = function(roomId) {
"Room.timelineReset", "Room.timelineReset",
"Room.localEchoUpdated", "Room.localEchoUpdated",
"Room.accountData", "Room.accountData",
"Room.myMembership",
]); ]);
this._registerStateListeners(room); this._registerStateListeners(room);
return room; return room;
@@ -976,9 +977,10 @@ SyncApi.prototype._processSyncResponse = async function(
// Handle invites // Handle invites
inviteRooms.forEach(function(inviteObj) { inviteRooms.forEach(function(inviteObj) {
const room = inviteObj.room; const room = inviteObj.room;
room.setSyncedMembership("invite");
const stateEvents = const stateEvents =
self._mapSyncEventsFormat(inviteObj.invite_state, room); self._mapSyncEventsFormat(inviteObj.invite_state, room);
room.updateMyMembership("invite");
self._processRoomEvents(room, stateEvents); self._processRoomEvents(room, stateEvents);
if (inviteObj.isBrandNewRoom) { if (inviteObj.isBrandNewRoom) {
room.recalculate(); room.recalculate();
@@ -993,7 +995,6 @@ SyncApi.prototype._processSyncResponse = async function(
// Handle joins // Handle joins
await Promise.mapSeries(joinRooms, async function(joinObj) { await Promise.mapSeries(joinRooms, async function(joinObj) {
const room = joinObj.room; const room = joinObj.room;
room.setSyncedMembership("join");
const stateEvents = self._mapSyncEventsFormat(joinObj.state, room); const stateEvents = self._mapSyncEventsFormat(joinObj.state, room);
const timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room); const timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room);
const ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral); const ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral);
@@ -1009,6 +1010,8 @@ SyncApi.prototype._processSyncResponse = async function(
); );
} }
room.updateMyMembership("join");
joinObj.timeline = joinObj.timeline || {}; joinObj.timeline = joinObj.timeline || {};
if (joinObj.isBrandNewRoom) { if (joinObj.isBrandNewRoom) {
@@ -1116,8 +1119,6 @@ SyncApi.prototype._processSyncResponse = async function(
// Handle leaves (e.g. kicked rooms) // Handle leaves (e.g. kicked rooms)
leaveRooms.forEach(function(leaveObj) { leaveRooms.forEach(function(leaveObj) {
const room = leaveObj.room; const room = leaveObj.room;
room.setSyncedMembership("leave");
const stateEvents = const stateEvents =
self._mapSyncEventsFormat(leaveObj.state, room); self._mapSyncEventsFormat(leaveObj.state, room);
const timelineEvents = const timelineEvents =
@@ -1125,6 +1126,8 @@ SyncApi.prototype._processSyncResponse = async function(
const accountDataEvents = const accountDataEvents =
self._mapSyncEventsFormat(leaveObj.account_data); self._mapSyncEventsFormat(leaveObj.account_data);
room.updateMyMembership("leave");
self._processRoomEvents(room, stateEvents, timelineEvents); self._processRoomEvents(room, stateEvents, timelineEvents);
room.addAccountData(accountDataEvents); room.addAccountData(accountDataEvents);
@@ -1145,8 +1148,6 @@ SyncApi.prototype._processSyncResponse = async function(
accountDataEvents.forEach(function(e) { accountDataEvents.forEach(function(e) {
client.emit("event", e); client.emit("event", e);
}); });
room.onLeft();
}); });
// update the notification timeline, if appropriate. // update the notification timeline, if appropriate.