From cb91c4292c4d6d906cae01c66f7acde579794f32 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 27 May 2021 16:00:12 +0100 Subject: [PATCH] Merge branch 'develop' into gsouquet/pr-review-linting-rules --- CHANGELOG.md | 54 ++++ package.json | 4 +- spec/integ/matrix-client-crypto.spec.js | 43 +-- spec/integ/megolm-integ.spec.js | 11 +- spec/olm-loader.js | 2 +- spec/test-utils.js | 23 +- spec/unit/utils.spec.js | 48 --- src/@types/event.ts | 10 +- src/@types/global.d.ts | 2 +- src/client.js | 55 +++- src/content-repo.js | 4 +- src/crypto/OutgoingRoomKeyRequestManager.js | 5 +- src/crypto/algorithms/megolm.js | 6 +- src/crypto/algorithms/olm.js | 2 +- src/crypto/index.js | 7 +- src/http-api.js | 7 +- src/index.ts | 1 - src/models/event-timeline-set.js | 24 +- src/models/event.js | 32 +- src/models/relations.js | 59 +++- src/models/room-member.js | 9 +- src/models/room-state.js | 14 +- src/models/room.js | 110 +++++-- src/models/search-result.js | 7 +- src/scheduler.js | 28 +- src/store/memory.js | 11 +- src/sync.js | 43 +-- src/utils.ts | 331 +------------------- src/webrtc/call.ts | 8 +- src/webrtc/callEventHandler.ts | 11 +- yarn.lock | 8 +- 31 files changed, 421 insertions(+), 558 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5dc8a32f..1af3ede1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,57 @@ +Changes in [11.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.1.0) (2021-05-24) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.1.0-rc.1...v11.1.0) + + * [Release] Bump libolm version and update package name + [\#1707](https://github.com/matrix-org/matrix-js-sdk/pull/1707) + * [Release] Change call event handlers to adapt to undecrypted events + [\#1699](https://github.com/matrix-org/matrix-js-sdk/pull/1699) + +Changes in [11.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.1.0-rc.1) (2021-05-19) +============================================================================================================ +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.0.0...v11.1.0-rc.1) + + * Fix regressed glare + [\#1690](https://github.com/matrix-org/matrix-js-sdk/pull/1690) + * Add m.reaction to EventType enum + [\#1692](https://github.com/matrix-org/matrix-js-sdk/pull/1692) + * Prioritise and reduce the amount of events decrypted on application startup + [\#1684](https://github.com/matrix-org/matrix-js-sdk/pull/1684) + * Decrypt relations before applying them to target event + [\#1696](https://github.com/matrix-org/matrix-js-sdk/pull/1696) + * Guard against duplicates in `Relations` model + +Changes in [11.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.0.0) (2021-05-17) +================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.0.0-rc.1...v11.0.0) + + * [Release] Fix regressed glare + [\#1695](https://github.com/matrix-org/matrix-js-sdk/pull/1695) + +Changes in [11.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.0.0-rc.1) (2021-05-11) +============================================================================================================ +[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.1.0...v11.0.0-rc.1) + +BREAKING CHANGES +--- + + * `MatrixCall` and related APIs have been redesigned to support multiple streams + (see [\#1660](https://github.com/matrix-org/matrix-js-sdk/pull/1660) for more details) + +All changes +--- + + * Switch from MSC1772 unstable prefixes to stable + [\#1679](https://github.com/matrix-org/matrix-js-sdk/pull/1679) + * Update the VoIP example to work with the new changes + [\#1680](https://github.com/matrix-org/matrix-js-sdk/pull/1680) + * Bump hosted-git-info from 2.8.8 to 2.8.9 + [\#1687](https://github.com/matrix-org/matrix-js-sdk/pull/1687) + * Support for multiple streams (not MSC3077) + [\#1660](https://github.com/matrix-org/matrix-js-sdk/pull/1660) + * Tweak missing m.room.create errors to describe their source + [\#1683](https://github.com/matrix-org/matrix-js-sdk/pull/1683) + Changes in [10.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.1.0) (2021-05-10) ================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.1.0-rc.1...v10.1.0) diff --git a/package.json b/package.json index d9ed10c73..3a2320efd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-js-sdk", - "version": "10.1.0", + "version": "11.1.0", "description": "Matrix Client-Server SDK for Javascript", "scripts": { "prepublishOnly": "yarn build", @@ -72,6 +72,7 @@ "@babel/preset-env": "^7.12.11", "@babel/preset-typescript": "^7.12.7", "@babel/register": "^7.12.10", + "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", "@types/jest": "^26.0.20", "@types/node": "12", "@types/request": "^2.48.5", @@ -91,7 +92,6 @@ "jest-localstorage-mock": "^2.4.6", "jsdoc": "^3.6.6", "matrix-mock-request": "^1.2.3", - "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", "rimraf": "^3.0.2", "terser": "^5.5.1", "tsify": "^5.0.2", diff --git a/spec/integ/matrix-client-crypto.spec.js b/spec/integ/matrix-client-crypto.spec.js index be9d725a8..c86e0e60e 100644 --- a/spec/integ/matrix-client-crypto.spec.js +++ b/spec/integ/matrix-client-crypto.spec.js @@ -28,11 +28,10 @@ limitations under the License. // load olm before the sdk if possible import '../olm-loader'; -import { logger } from '../../src/logger'; +import {logger} from '../../src/logger'; import * as testUtils from "../test-utils"; -import * as utils from "../../src/utils"; -import { TestClient } from "../TestClient"; -import { CRYPTO_ENABLED } from "../../src/client"; +import {TestClient} from "../TestClient"; +import {CRYPTO_ENABLED} from "../../src/client"; let aliTestClient; const roomId = "!room:localhost"; @@ -76,7 +75,7 @@ function expectAliQueryKeys() { ); const result = {}; result[bobUserId] = bobKeys; - return { device_keys: result }; + return {device_keys: result}; }); return aliTestClient.httpBackend.flush("/keys/query", 1); } @@ -104,7 +103,7 @@ function expectBobQueryKeys() { ); const result = {}; result[aliUserId] = aliKeys; - return { device_keys: result }; + return {device_keys: result}; }); return bobTestClient.httpBackend.flush("/keys/query", 1); } @@ -133,7 +132,7 @@ function expectAliClaimKeys() { result[bobUserId] = {}; result[bobUserId][bobDeviceId] = {}; result[bobUserId][bobDeviceId][keyId] = keys[keyId]; - return { one_time_keys: result }; + return {one_time_keys: result}; }); }).then(() => { // it can take a while to process the key query, so give it some extra @@ -145,6 +144,7 @@ function expectAliClaimKeys() { }); } + function aliDownloadsKeys() { // can't query keys before bob has uploaded them expect(bobTestClient.getSigningKey()).toBeTruthy(); @@ -243,7 +243,7 @@ function bobSendsReplyMessage() { function expectAliSendMessageRequest() { return expectSendMessageRequest(aliTestClient.httpBackend).then(function(content) { aliMessages.push(content); - expect(utils.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]); + expect(Object.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]); const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()]; expect(ciphertext).toBeTruthy(); return ciphertext; @@ -260,7 +260,7 @@ function expectBobSendMessageRequest() { bobMessages.push(content); const aliKeyId = "curve25519:" + aliDeviceId; const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId]; - expect(utils.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]); + expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]); const ciphertext = content.ciphertext[aliDeviceCurve25519Key]; expect(ciphertext).toBeTruthy(); return ciphertext; @@ -269,7 +269,7 @@ function expectBobSendMessageRequest() { function sendMessage(client) { return client.sendMessage( - roomId, { msgtype: "m.text", body: "Hello, World" }, + roomId, {msgtype: "m.text", body: "Hello, World"}, ); } @@ -357,6 +357,7 @@ function recvMessage(httpBackend, client, sender, message) { }); } + /** * Send an initial sync response to the client (which just includes the member * list for our test room). @@ -394,6 +395,7 @@ function firstSync(testClient) { return testClient.flushSync(); } + describe("MatrixClient crypto", function() { if (!CRYPTO_ENABLED) { return; @@ -475,7 +477,7 @@ describe("MatrixClient crypto", function() { ).respond(200, function(path, content) { const result = {}; result[bobUserId] = bobKeys; - return { device_keys: result }; + return {device_keys: result}; }); return Promise.all([ @@ -517,7 +519,7 @@ describe("MatrixClient crypto", function() { ).respond(200, function(path, content) { const result = {}; result[bobUserId] = bobKeys; - return { device_keys: result }; + return {device_keys: result}; }); return Promise.all([ @@ -531,6 +533,7 @@ describe("MatrixClient crypto", function() { }); }); + it("Bob starts his client and uploads device keys and one-time keys", function() { return Promise.resolve() .then(() => bobTestClient.start()) @@ -542,7 +545,7 @@ describe("MatrixClient crypto", function() { }); it("Ali sends a message", function() { - aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } }); + aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}}); return Promise.resolve() .then(() => aliTestClient.start()) .then(() => bobTestClient.start()) @@ -552,7 +555,7 @@ describe("MatrixClient crypto", function() { }); it("Bob receives a message", function() { - aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } }); + aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}}); return Promise.resolve() .then(() => aliTestClient.start()) .then(() => bobTestClient.start()) @@ -563,7 +566,7 @@ describe("MatrixClient crypto", function() { }); it("Bob receives a message with a bogus sender", function() { - aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } }); + aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}}); return Promise.resolve() .then(() => aliTestClient.start()) .then(() => bobTestClient.start()) @@ -617,7 +620,7 @@ describe("MatrixClient crypto", function() { }); it("Ali blocks Bob's device", function() { - aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } }); + aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}}); return Promise.resolve() .then(() => aliTestClient.start()) .then(() => bobTestClient.start()) @@ -637,7 +640,7 @@ describe("MatrixClient crypto", function() { }); it("Bob receives two pre-key messages", function() { - aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } }); + aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}}); return Promise.resolve() .then(() => aliTestClient.start()) .then(() => bobTestClient.start()) @@ -650,8 +653,8 @@ describe("MatrixClient crypto", function() { }); it("Bob replies to the message", function() { - aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } }); - bobTestClient.expectKeyQuery({ device_keys: { [bobUserId]: {} } }); + aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}}); + bobTestClient.expectKeyQuery({device_keys: {[bobUserId]: {}}}); return Promise.resolve() .then(() => aliTestClient.start()) .then(() => bobTestClient.start()) @@ -669,7 +672,7 @@ describe("MatrixClient crypto", function() { it("Ali does a key query when encryption is enabled", function() { // enabling encryption in the room should make alice download devices // for both members. - aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } }); + aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}}); return Promise.resolve() .then(() => aliTestClient.start()) .then(() => firstSync(aliTestClient)) diff --git a/spec/integ/megolm-integ.spec.js b/spec/integ/megolm-integ.spec.js index 6edbcc49e..8461bc385 100644 --- a/spec/integ/megolm-integ.spec.js +++ b/spec/integ/megolm-integ.spec.js @@ -16,7 +16,6 @@ limitations under the License. */ import anotherjson from "another-json"; -import * as utils from "../../src/utils"; import * as testUtils from "../test-utils"; import { TestClient } from "../TestClient"; import { logger } from "../../src/logger"; @@ -32,7 +31,7 @@ const ROOM_ID = "!room:id"; */ function createOlmSession(olmAccount, recipientTestClient) { return recipientTestClient.awaitOneTimeKeyUpload().then((keys) => { - const otkId = utils.keys(keys)[0]; + const otkId = Object.keys(keys)[0]; const otk = keys[otkId]; const session = new global.Olm.Session(); @@ -256,7 +255,7 @@ describe("megolm", function() { const testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys()); testOlmAccount.mark_keys_as_published(); - const keyId = utils.keys(testOneTimeKeys.curve25519)[0]; + const keyId = Object.keys(testOneTimeKeys.curve25519)[0]; const oneTimeKey = testOneTimeKeys.curve25519[keyId]; const keyResult = { 'key': oneTimeKey, @@ -483,8 +482,9 @@ describe("megolm", function() { return aliceTestClient.flushSync().then(() => { return aliceTestClient.flushSync(); }); - }).then(function() { + }).then(async function() { const room = aliceTestClient.client.getRoom(ROOM_ID); + await room.decryptCriticalEvents(); const event = room.getLiveTimeline().getEvents()[0]; expect(event.getContent().body).toEqual('42'); }); @@ -930,8 +930,9 @@ describe("megolm", function() { aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse); return aliceTestClient.flushSync(); - }).then(function() { + }).then(async function() { const room = aliceTestClient.client.getRoom(ROOM_ID); + await room.decryptCriticalEvents(); const event = room.getLiveTimeline().getEvents()[0]; expect(event.getContent().body).toEqual('42'); diff --git a/spec/olm-loader.js b/spec/olm-loader.js index 639a97b9e..505c08615 100644 --- a/spec/olm-loader.js +++ b/spec/olm-loader.js @@ -20,7 +20,7 @@ import * as utils from "../src/utils"; // try to load the olm library. try { - global.Olm = require('olm'); + global.Olm = require('@matrix-org/olm'); logger.log('loaded libolm'); } catch (e) { logger.warn("unable to run crypto tests: libolm not available"); diff --git a/spec/test-utils.js b/spec/test-utils.js index 6c7679432..d308b6d35 100644 --- a/spec/test-utils.js +++ b/spec/test-utils.js @@ -210,18 +210,21 @@ MockStorageApi.prototype = { * @returns {Promise} promise which resolves (to `event`) when the event has been decrypted */ export function awaitDecryption(event) { - if (!event.isBeingDecrypted()) { - return Promise.resolve(event); - } + // An event is not always decrypted ahead of time + // getClearContent is a good signal to know whether an event has been decrypted + // already + if (event.getClearContent() !== null) { + return event; + } else { + logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`); - logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`); - - return new Promise((resolve, reject) => { - event.once('Event.decrypted', (ev) => { - logger.log(`${Date.now()} event ${event.getId()} now decrypted`); - resolve(ev); + return new Promise((resolve, reject) => { + event.once('Event.decrypted', (ev) => { + logger.log(`${Date.now()} event ${event.getId()} now decrypted`); + resolve(ev); + }); }); - }); + } } export function HttpResponse( diff --git a/spec/unit/utils.spec.js b/spec/unit/utils.spec.js index 62201f06d..703326f46 100644 --- a/spec/unit/utils.spec.js +++ b/spec/unit/utils.spec.js @@ -26,40 +26,6 @@ describe("utils", function() { }); }); - describe("forEach", function() { - it("should be invoked for each element", function() { - const arr = []; - utils.forEach([55, 66, 77], function(element) { - arr.push(element); - }); - expect(arr).toEqual([55, 66, 77]); - }); - }); - - describe("findElement", function() { - it("should find only 1 element if there is a match", function() { - const matchFn = function() { - return true; - }; - const arr = [55, 66, 77]; - expect(utils.findElement(arr, matchFn)).toEqual(55); - }); - it("should be able to find in reverse order", function() { - const matchFn = function() { - return true; - }; - const arr = [55, 66, 77]; - expect(utils.findElement(arr, matchFn, true)).toEqual(77); - }); - it("should find nothing if the function never returns true", function() { - const matchFn = function() { - return false; - }; - const arr = [55, 66, 77]; - expect(utils.findElement(arr, matchFn)).toBeFalsy(); - }); - }); - describe("removeElement", function() { it("should remove only 1 element if there is a match", function() { const matchFn = function() { @@ -103,20 +69,6 @@ describe("utils", function() { }); }); - describe("isArray", function() { - it("should return true for arrays", function() { - expect(utils.isArray([])).toBe(true); - expect(utils.isArray([5, 3, 7])).toBe(true); - - expect(utils.isArray()).toBe(false); - expect(utils.isArray(null)).toBe(false); - expect(utils.isArray({})).toBe(false); - expect(utils.isArray("foo")).toBe(false); - expect(utils.isArray(555)).toBe(false); - expect(utils.isArray(function() {})).toBe(false); - }); - }); - describe("checkObjectHasKeys", function() { it("should throw for missing keys", function() { expect(function() { diff --git a/src/@types/event.ts b/src/@types/event.ts index a908088d7..3c905442b 100644 --- a/src/@types/event.ts +++ b/src/@types/event.ts @@ -36,9 +36,8 @@ export enum EventType { */ RoomAliases = "m.room.aliases", // deprecated https://matrix.org/docs/spec/client_server/r0.6.1#historical-events - // Spaces MSC1772 - SpaceChild = "org.matrix.msc1772.space.child", - SpaceParent = "org.matrix.msc1772.space.parent", + SpaceChild = "m.space.child", + SpaceParent = "m.space.parent", // Room timeline events RoomRedaction = "m.room.redaction", @@ -62,6 +61,7 @@ export enum EventType { KeyVerificationDone = "m.key.verification.done", // use of this is discouraged https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-feedback RoomMessageFeedback = "m.room.message.feedback", + Reaction = "m.reaction", // Room ephemeral events Typing = "m.typing", @@ -95,8 +95,8 @@ export enum MsgType { Video = "m.video", } -export const RoomCreateTypeField = "org.matrix.msc1772.type"; // Spaces MSC1772 +export const RoomCreateTypeField = "type"; export enum RoomType { - Space = "org.matrix.msc1772.space", // Spaces MSC1772 + Space = "m.space", } diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index dd437247a..337768428 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -15,7 +15,7 @@ limitations under the License. */ // this is needed to tell TS about global.Olm -import * as Olm from "olm"; // eslint-disable-line @typescript-eslint/no-unused-vars +import * as Olm from "@matrix-org/olm"; // eslint-disable-line @typescript-eslint/no-unused-vars export {}; diff --git a/src/client.js b/src/client.js index c3ec7710d..cb60705ef 100644 --- a/src/client.js +++ b/src/client.js @@ -352,6 +352,10 @@ export function MatrixClient(opts) { if (call) { this._callEventHandler = new CallEventHandler(this); this._supportsVoip = true; + // Start listening for calls after the initial sync is done + // We do not need to backfill the call event buffer + // with encrypted events that might never get decrypted + this.on("sync", this._startCallEventHandler); } else { this._callEventHandler = null; } @@ -3971,9 +3975,9 @@ MatrixClient.prototype.scrollback = function(room, limit, callback) { limit, 'b'); }).then(function(res) { - const matrixEvents = utils.map(res.chunk, _PojoToMatrixEventMapper(self)); + const matrixEvents = res.chunk.map(_PojoToMatrixEventMapper(self)); if (res.state) { - const stateEvents = utils.map(res.state, _PojoToMatrixEventMapper(self)); + const stateEvents = res.state.map(_PojoToMatrixEventMapper(self)); room.currentState.setUnknownStateEvents(stateEvents); } room.addEventsToTimeline(matrixEvents, true, room.getLiveTimeline()); @@ -4062,16 +4066,16 @@ MatrixClient.prototype.getEventTimeline = function(timelineSet, eventId) { const events = res.events_after .concat([res.event]) .concat(res.events_before); - const matrixEvents = utils.map(events, self.getEventMapper()); + const matrixEvents = events.map(self.getEventMapper()); let timeline = timelineSet.getTimelineForEvent(matrixEvents[0].getId()); if (!timeline) { timeline = timelineSet.addTimeline(); - timeline.initialiseState(utils.map(res.state, + timeline.initialiseState(res.state.map( self.getEventMapper())); timeline.getState(EventTimeline.FORWARDS).paginationToken = res.end; } else { - const stateEvents = utils.map(res.state, self.getEventMapper()); + const stateEvents = res.state.map(self.getEventMapper()); timeline.getState(EventTimeline.BACKWARDS).setUnknownStateEvents(stateEvents); } timelineSet.addEventsToTimeline(matrixEvents, true, timeline, res.start); @@ -4234,11 +4238,11 @@ MatrixClient.prototype.paginateEventTimeline = function(eventTimeline, opts) { promise.then(function(res) { if (res.state) { const roomState = eventTimeline.getState(dir); - const stateEvents = utils.map(res.state, self.getEventMapper()); + const stateEvents = res.state.map(self.getEventMapper()); roomState.setUnknownStateEvents(stateEvents); } const token = res.end; - const matrixEvents = utils.map(res.chunk, self.getEventMapper()); + const matrixEvents = res.chunk.map(self.getEventMapper()); eventTimeline.getTimelineSet() .addEventsToTimeline(matrixEvents, backwards, eventTimeline, token); @@ -4977,6 +4981,13 @@ MatrixClient.prototype.getOpenIdToken = function() { // VoIP operations // =============== +MatrixClient.prototype._startCallEventHandler = function() { + if (this.isInitialSyncComplete()) { + this._callEventHandler.start(); + this.off("sync", this._startCallEventHandler); + } +}; + /** * @param {module:client.callback} callback Optional. * @return {Promise} Resolves: TODO @@ -5544,8 +5555,9 @@ function _resolve(callback, resolve, res) { resolve(res); } -function _PojoToMatrixEventMapper(client, options) { - const preventReEmit = Boolean(options && options.preventReEmit); +function _PojoToMatrixEventMapper(client, options = {}) { + const preventReEmit = Boolean(options.preventReEmit); + const decrypt = options.decrypt !== false; function mapper(plainOldJsObject) { const event = new MatrixEvent(plainOldJsObject); if (event.isEncrypted()) { @@ -5554,7 +5566,9 @@ function _PojoToMatrixEventMapper(client, options) { "Event.decrypted", ]); } - event.attemptDecryption(client._crypto); + if (decrypt) { + client.decryptEventIfNeeded(event); + } } if (!preventReEmit) { client.reEmitter.reEmit(event, ["Event.replaced"]); @@ -5567,6 +5581,7 @@ function _PojoToMatrixEventMapper(client, options) { /** * @param {object} [options] * @param {bool} options.preventReEmit don't reemit events emitted on an event mapped by this mapper on the client + * @param {bool} options.decrypt decrypt event proactively * @return {Function} */ MatrixClient.prototype.getEventMapper = function(options = undefined) { @@ -5594,6 +5609,26 @@ MatrixClient.prototype.generateClientSecret = function() { return randomString(32); }; +/** + * Attempts to decrypt an event + * @param {MatrixEvent} event The event to decrypt + * @returns {Promise} A decryption promise + * @param {object} options + * @param {bool} options.isRetry True if this is a retry (enables more logging) + * @param {bool} options.emit Emits "event.decrypted" if set to true + */ +MatrixClient.prototype.decryptEventIfNeeded = function(event, options) { + if (event.shouldAttemptDecryption()) { + event.attemptDecryption(this._crypto, options); + } + + if (event.isBeingDecrypted()) { + return event._decryptionPromise; + } else { + return Promise.resolve(); + } +}; + // MatrixClient Event JSDocs /** diff --git a/src/content-repo.js b/src/content-repo.js index 0d1ae1ea0..1b92d59ae 100644 --- a/src/content-repo.js +++ b/src/content-repo.js @@ -59,7 +59,7 @@ export function getHttpUriForMxc(baseUrl, mxc, width, height, if (resizeMethod) { params.method = resizeMethod; } - if (utils.keys(params).length > 0) { + if (Object.keys(params).length > 0) { // these are thumbnailing params so they probably want the // thumbnailing API... prefix = "/_matrix/media/r0/thumbnail/"; @@ -72,6 +72,6 @@ export function getHttpUriForMxc(baseUrl, mxc, width, height, serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset); } return baseUrl + prefix + serverAndMediaId + - (utils.keys(params).length === 0 ? "" : + (Object.keys(params).length === 0 ? "" : ("?" + utils.encodeParams(params))) + fragment; } diff --git a/src/crypto/OutgoingRoomKeyRequestManager.js b/src/crypto/OutgoingRoomKeyRequestManager.js index 94bd45d23..899c3ca96 100644 --- a/src/crypto/OutgoingRoomKeyRequestManager.js +++ b/src/crypto/OutgoingRoomKeyRequestManager.js @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { logger } from '../logger'; -import * as utils from '../utils'; +import {logger} from '../logger'; /** * Internal module. Management of outgoing room key requests. @@ -496,7 +495,7 @@ function stringifyRequestBody(requestBody) { function stringifyRecipientList(recipients) { return '[' - + utils.map(recipients, (r) => `${r.userId}:${r.deviceId}`).join(",") + + recipients.map((r) => `${r.userId}:${r.deviceId}`).join(",") + ']'; } diff --git a/src/crypto/algorithms/megolm.js b/src/crypto/algorithms/megolm.js index c4e987f0a..0d97b2450 100644 --- a/src/crypto/algorithms/megolm.js +++ b/src/crypto/algorithms/megolm.js @@ -1074,7 +1074,7 @@ MegolmEncryption.prototype._removeUnknownDevices = function(devicesInRoom) { */ MegolmEncryption.prototype._getDevicesInRoom = async function(room) { const members = await room.getEncryptionTargetMembers(); - const roomMembers = utils.map(members, function(u) { + const roomMembers = members.map(function(u) { return u.userId; }); @@ -1368,7 +1368,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) { if (event.getType() == "m.forwarded_room_key") { exportFormat = true; forwardingKeyChain = content.forwarding_curve25519_key_chain; - if (!utils.isArray(forwardingKeyChain)) { + if (!Array.isArray(forwardingKeyChain)) { forwardingKeyChain = []; } @@ -1689,7 +1689,7 @@ MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionI await Promise.all([...pending].map(async (ev) => { try { - await ev.attemptDecryption(this._crypto, true); + await ev.attemptDecryption(this._crypto, { isRetry: true }); } catch (e) { // don't die if something goes wrong } diff --git a/src/crypto/algorithms/olm.js b/src/crypto/algorithms/olm.js index 56c316e88..74444b75a 100644 --- a/src/crypto/algorithms/olm.js +++ b/src/crypto/algorithms/olm.js @@ -95,7 +95,7 @@ OlmEncryption.prototype.encryptMessage = async function(room, eventType, content const members = await room.getEncryptionTargetMembers(); - const users = utils.map(members, function(u) { + const users = members.map(function(u) { return u.userId; }); diff --git a/src/crypto/index.js b/src/crypto/index.js index be7896321..ae8de2af1 100644 --- a/src/crypto/index.js +++ b/src/crypto/index.js @@ -186,7 +186,7 @@ export function Crypto(baseApis, sessionStore, userId, deviceId, // map from algorithm to DecryptionAlgorithm instance, for each room this._roomDecryptors = {}; - this._supportedAlgorithms = utils.keys( + this._supportedAlgorithms = Object.keys( algorithms.DECRYPTION_CLASSES, ); @@ -3317,7 +3317,10 @@ Crypto.prototype._onToDeviceEvent = function(event) { this._onKeyVerificationMessage(event); } else if (event.getContent().msgtype === "m.bad.encrypted") { this._onToDeviceBadEncrypted(event); - } else if (event.isBeingDecrypted()) { + } else if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) { + if (!event.isBeingDecrypted()) { + event.attemptDecryption(this); + } // once the event has been decrypted, try again event.once('Event.decrypted', (ev) => { this._onToDeviceEvent(ev); diff --git a/src/http-api.js b/src/http-api.js index d723e5b98..a48c165d0 100644 --- a/src/http-api.js +++ b/src/http-api.js @@ -803,7 +803,8 @@ const requestCallback = function( } if (!err) { try { - if (response.statusCode >= 400) { + const httpStatus = response.status || response.statusCode; // XMLHttpRequest vs http.IncomingMessage + if (httpStatus >= 400) { err = parseErrorResponse(response, body); } else if (bodyParser) { body = bodyParser(body); @@ -818,7 +819,7 @@ const requestCallback = function( userDefinedCallback(err); } else { const res = { - code: response.statusCode, + code: response.status || response.statusCode, // XMLHttpRequest vs http.IncomingMessage // XXX: why do we bother with this? it doesn't work for // XMLHttpRequest, so clearly we don't use it. @@ -842,7 +843,7 @@ const requestCallback = function( * @returns {Error} */ function parseErrorResponse(response, body) { - const httpStatus = response.statusCode; + const httpStatus = response.status || response.statusCode; // XMLHttpRequest vs http.IncomingMessage const contentType = getResponseContentType(response); let err; diff --git a/src/index.ts b/src/index.ts index 29edc8bb0..e599e0065 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,7 +19,6 @@ import * as utils from "./utils"; import request from "request"; matrixcs.request(request); -utils.runPolyfills(); try { // eslint-disable-next-line @typescript-eslint/no-var-requires diff --git a/src/models/event-timeline-set.js b/src/models/event-timeline-set.js index 13316ea7d..5835e5343 100644 --- a/src/models/event-timeline-set.js +++ b/src/models/event-timeline-set.js @@ -249,7 +249,7 @@ EventTimelineSet.prototype.findEventById = function(eventId) { if (!tl) { return undefined; } - return utils.findElement(tl.getEvents(), function(ev) { + return tl.getEvents().find(function(ev) { return ev.getId() == eventId; }); }; @@ -739,16 +739,11 @@ EventTimelineSet.prototype.setRelationsTarget = function(event) { if (!relationsForEvent) { return; } - // don't need it for non m.replace relations for now - const relationsWithRelType = relationsForEvent["m.replace"]; - if (!relationsWithRelType) { - return; - } - // only doing replacements for messages for now (e.g. edits) - const relationsWithEventType = relationsWithRelType["m.room.message"]; - if (relationsWithEventType) { - relationsWithEventType.setTargetEvent(event); + for (const relationsWithRelType of Object.values(relationsForEvent)) { + for (const relationsWithEventType of Object.values(relationsWithRelType)) { + relationsWithEventType.setTargetEvent(event); + } } }; @@ -768,7 +763,7 @@ EventTimelineSet.prototype.aggregateRelations = function(event) { } // If the event is currently encrypted, wait until it has been decrypted. - if (event.isBeingDecrypted()) { + if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) { event.once("Event.decrypted", () => { this.aggregateRelations(event); }); @@ -796,7 +791,6 @@ EventTimelineSet.prototype.aggregateRelations = function(event) { } let relationsWithEventType = relationsWithRelType[eventType]; - let isNewRelations = false; let relatesToEvent; if (!relationsWithEventType) { relationsWithEventType = relationsWithRelType[eventType] = new Relations( @@ -804,7 +798,6 @@ EventTimelineSet.prototype.aggregateRelations = function(event) { eventType, this.room, ); - isNewRelations = true; relatesToEvent = this.findEventById(relatesToEventId) || this.room.getPendingEvent(relatesToEventId); if (relatesToEvent) { relationsWithEventType.setTargetEvent(relatesToEvent); @@ -812,11 +805,6 @@ EventTimelineSet.prototype.aggregateRelations = function(event) { } relationsWithEventType.addEvent(event); - - // only emit once event has been added to relations - if (isNewRelations && relatesToEvent) { - relatesToEvent.emit("Event.relationsCreated", relationType, eventType); - } }; /** diff --git a/src/models/event.js b/src/models/event.js index b7e22a606..1b3bec755 100644 --- a/src/models/event.js +++ b/src/models/event.js @@ -398,6 +398,12 @@ utils.extend(MatrixEvent.prototype, { this._clearEvent.content.msgtype === "m.bad.encrypted"; }, + shouldAttemptDecryption: function() { + return this.isEncrypted() + && !this.isBeingDecrypted() + && this.getClearContent() === null; + }, + /** * Start the process of trying to decrypt this event. * @@ -406,12 +412,22 @@ utils.extend(MatrixEvent.prototype, { * @internal * * @param {module:crypto} crypto crypto module - * @param {bool} isRetry True if this is a retry (enables more logging) + * @param {object} options + * @param {bool} options.isRetry True if this is a retry (enables more logging) + * @param {bool} options.emit Emits "event.decrypted" if set to true * * @returns {Promise} promise which resolves (to undefined) when the decryption * attempt is completed. */ - attemptDecryption: async function(crypto, isRetry) { + attemptDecryption: async function(crypto, options = {}) { + // For backwards compatibility purposes + // The function signature used to be attemptDecryption(crypto, isRetry) + if (typeof options === "boolean") { + options = { + isRetry: options, + }; + } + // start with a couple of sanity checks. if (!this.isEncrypted()) { throw new Error("Attempt to decrypt event which isn't encrypted"); @@ -441,7 +457,7 @@ utils.extend(MatrixEvent.prototype, { return this._decryptionPromise; } - this._decryptionPromise = this._decryptionLoop(crypto, isRetry); + this._decryptionPromise = this._decryptionLoop(crypto, options); return this._decryptionPromise; }, @@ -486,7 +502,7 @@ utils.extend(MatrixEvent.prototype, { return recipients; }, - _decryptionLoop: async function(crypto, isRetry) { + _decryptionLoop: async function(crypto, options = {}) { // 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 @@ -503,7 +519,7 @@ utils.extend(MatrixEvent.prototype, { res = this._badEncryptedMessage("Encryption not enabled"); } else { res = await crypto.decryptEvent(this); - if (isRetry) { + if (options.isRetry === true) { logger.info(`Decrypted event on retry (id=${this.getId()})`); } } @@ -511,7 +527,7 @@ utils.extend(MatrixEvent.prototype, { if (e.name !== "DecryptionError") { // not a decryption error: log the whole exception as an error // (and don't bother with a retry) - const re = isRetry ? 're' : ''; + const re = options.isRetry ? 're' : ''; logger.error( `Error ${re}decrypting event ` + `(id=${this.getId()}): ${e.stack || e}`, @@ -577,7 +593,9 @@ utils.extend(MatrixEvent.prototype, { // pick up the wrong contents. this.setPushActions(null); - this.emit("Event.decrypted", this, err); + if (options.emit !== false) { + this.emit("Event.decrypted", this, err); + } return; } diff --git a/src/models/relations.js b/src/models/relations.js index 80d894ce8..1e24d534e 100644 --- a/src/models/relations.js +++ b/src/models/relations.js @@ -41,11 +41,14 @@ export class Relations extends EventEmitter { super(); this.relationType = relationType; this.eventType = eventType; + this._relationEventIds = new Set(); this._relations = new Set(); this._annotationsByKey = {}; this._annotationsBySender = {}; this._sortedAnnotationsByKey = []; this._targetEvent = null; + this._room = room; + this._creationEmitted = false; } /** @@ -54,8 +57,8 @@ export class Relations extends EventEmitter { * @param {MatrixEvent} event * The new relation event to be added. */ - addEvent(event) { - if (this._relations.has(event)) { + async addEvent(event) { + if (this._relationEventIds.has(event.getId())) { return; } @@ -80,16 +83,20 @@ export class Relations extends EventEmitter { } this._relations.add(event); + this._relationEventIds.add(event.getId()); if (this.relationType === "m.annotation") { this._addAnnotationToAggregation(event); } else if (this.relationType === "m.replace" && this._targetEvent) { - this._targetEvent.makeReplaced(this.getLastReplacement()); + const lastReplacement = await this.getLastReplacement(); + this._targetEvent.makeReplaced(lastReplacement); } event.on("Event.beforeRedaction", this._onBeforeRedaction); this.emit("Relations.add", event); + + this._maybeEmitCreated(); } /** @@ -98,7 +105,7 @@ export class Relations extends EventEmitter { * @param {MatrixEvent} event * The relation event to remove. */ - _removeEvent(event) { + async _removeEvent(event) { if (!this._relations.has(event)) { return; } @@ -122,7 +129,8 @@ export class Relations extends EventEmitter { if (this.relationType === "m.annotation") { this._removeAnnotationFromAggregation(event); } else if (this.relationType === "m.replace" && this._targetEvent) { - this._targetEvent.makeReplaced(this.getLastReplacement()); + const lastReplacement = await this.getLastReplacement(); + this._targetEvent.makeReplaced(lastReplacement); } this.emit("Relations.remove", event); @@ -227,7 +235,7 @@ export class Relations extends EventEmitter { * @param {MatrixEvent} redactedEvent * The original relation event that is about to be redacted. */ - _onBeforeRedaction = (redactedEvent) => { + _onBeforeRedaction = async (redactedEvent) => { if (!this._relations.has(redactedEvent)) { return; } @@ -238,7 +246,8 @@ export class Relations extends EventEmitter { // Remove the redacted annotation from aggregation by key this._removeAnnotationFromAggregation(redactedEvent); } else if (this.relationType === "m.replace" && this._targetEvent) { - this._targetEvent.makeReplaced(this.getLastReplacement()); + const lastReplacement = await this.getLastReplacement(); + this._targetEvent.makeReplaced(lastReplacement); } redactedEvent.removeListener("Event.beforeRedaction", this._onBeforeRedaction); @@ -291,7 +300,7 @@ export class Relations extends EventEmitter { * * @return {MatrixEvent?} */ - getLastReplacement() { + async getLastReplacement() { if (this.relationType !== "m.replace") { // Aggregating on last only makes sense for this relation type return null; @@ -309,7 +318,7 @@ export class Relations extends EventEmitter { this._targetEvent.getServerAggregatedRelation("m.replace"); const minTs = replaceRelation && replaceRelation.origin_server_ts; - return this.getRelations().reduce((last, event) => { + const lastReplacement = this.getRelations().reduce((last, event) => { if (event.getSender() !== this._targetEvent.getSender()) { return last; } @@ -321,23 +330,51 @@ export class Relations extends EventEmitter { } return event; }, null); + + if (lastReplacement?.shouldAttemptDecryption()) { + await lastReplacement.attemptDecryption(this._room._client._crypto); + } else if (lastReplacement?.isBeingDecrypted()) { + await lastReplacement._decryptionPromise; + } + + return lastReplacement; } /* * @param {MatrixEvent} targetEvent the event the relations are related to. */ - setTargetEvent(event) { + async setTargetEvent(event) { if (this._targetEvent) { return; } this._targetEvent = event; + if (this.relationType === "m.replace") { - const replacement = this.getLastReplacement(); + const replacement = await this.getLastReplacement(); // this is the initial update, so only call it if we already have something // to not emit Event.replaced needlessly if (replacement) { this._targetEvent.makeReplaced(replacement); } } + + this._maybeEmitCreated(); + } + + _maybeEmitCreated() { + if (this._creationEmitted) { + return; + } + // Only emit we're "created" once we have a target event instance _and_ + // at least one related event. + if (!this._targetEvent || !this._relations.size) { + return; + } + this._creationEmitted = true; + this._targetEvent.emit( + "Event.relationsCreated", + this.relationType, + this.eventType, + ); } } diff --git a/src/models/room-member.js b/src/models/room-member.js index 586f12e31..32ed9dc5d 100644 --- a/src/models/room-member.js +++ b/src/models/room-member.js @@ -133,14 +133,15 @@ RoomMember.prototype.setPowerLevelEvent = function(powerLevelEvent) { const evContent = powerLevelEvent.getDirectionalContent(); let maxLevel = evContent.users_default || 0; - utils.forEach(utils.values(evContent.users), function(lvl) { + const users = evContent.users || {}; + Object.values(users).forEach(function(lvl) { maxLevel = Math.max(maxLevel, lvl); }); const oldPowerLevel = this.powerLevel; const oldPowerLevelNorm = this.powerLevelNorm; - if (evContent.users && evContent.users[this.userId] !== undefined) { - this.powerLevel = evContent.users[this.userId]; + if (users[this.userId] !== undefined) { + this.powerLevel = users[this.userId]; } else if (evContent.users_default !== undefined) { this.powerLevel = evContent.users_default; } else { @@ -172,7 +173,7 @@ RoomMember.prototype.setTypingEvent = function(event) { const oldTyping = this.typing; this.typing = false; const typingList = event.getContent().user_ids; - if (!utils.isArray(typingList)) { + if (!Array.isArray(typingList)) { // malformed event :/ bail early. TODO: whine? return; } diff --git a/src/models/room-state.js b/src/models/room-state.js index 5bf674f9c..e991df57c 100644 --- a/src/models/room-state.js +++ b/src/models/room-state.js @@ -154,7 +154,7 @@ RoomState.prototype.setInvitedMemberCount = function(count) { * @return {Array} A list of RoomMembers. */ RoomState.prototype.getMembers = function() { - return utils.values(this.members); + return Object.values(this.members); }; /** @@ -163,7 +163,7 @@ RoomState.prototype.getMembers = function() { * @return {Array} A list of RoomMembers. */ RoomState.prototype.getMembersExcept = function(excludedIds) { - return utils.values(this.members) + return Object.values(this.members) .filter((m) => !excludedIds.includes(m.userId)); }; @@ -296,7 +296,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) { this._updateModifiedTime(); // update the core event dict - utils.forEach(stateEvents, function(event) { + stateEvents.forEach(function(event) { if (event.getRoomId() !== self.roomId) { return; } @@ -319,7 +319,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) { // core event dict as these structures may depend on other state events in // the given array (e.g. disambiguating display names in one go to do both // clashing names rather than progressively which only catches 1 of them). - utils.forEach(stateEvents, function(event) { + stateEvents.forEach(function(event) { if (event.getRoomId() !== self.roomId) { return; } @@ -349,8 +349,8 @@ RoomState.prototype.setStateEvents = function(stateEvents) { self._updateMember(member); self.emit("RoomState.members", event, self, member); } else if (event.getType() === "m.room.power_levels") { - const members = utils.values(self.members); - utils.forEach(members, function(member) { + const members = Object.values(self.members); + members.forEach(function(member) { // We only propagate `RoomState.members` event if the // power levels has been changed // large room suffer from large re-rendering especially when not needed @@ -511,7 +511,7 @@ RoomState.prototype._setOutOfBandMember = function(stateEvent) { * @param {MatrixEvent} event The typing event */ RoomState.prototype.setTypingEvent = function(event) { - utils.forEach(utils.values(this.members), function(member) { + Object.values(this.members).forEach(function(member) { member.setTypingEvent(event); }); }; diff --git a/src/models/room.js b/src/models/room.js index 22b04891b..8273d0e8e 100644 --- a/src/models/room.js +++ b/src/models/room.js @@ -20,17 +20,18 @@ limitations under the License. * @module models/room */ -import { EventEmitter } from "events"; -import { EventTimelineSet } from "./event-timeline-set"; -import { EventTimeline } from "./event-timeline"; -import { getHttpUriForMxc } from "../content-repo"; +import {EventEmitter} from "events"; +import {EventTimelineSet} from "./event-timeline-set"; +import {EventTimeline} from "./event-timeline"; +import {getHttpUriForMxc} from "../content-repo"; import * as utils from "../utils"; -import { EventStatus, MatrixEvent } from "./event"; -import { RoomMember } from "./room-member"; -import { RoomSummary } from "./room-summary"; -import { logger } from '../logger'; -import { ReEmitter } from '../ReEmitter'; -import { EventType, RoomCreateTypeField, RoomType } from "../@types/event"; +import {EventStatus, MatrixEvent} from "./event"; +import {RoomMember} from "./room-member"; +import {RoomSummary} from "./room-summary"; +import {logger} from '../logger'; +import {ReEmitter} from '../ReEmitter'; +import {EventType, RoomCreateTypeField, RoomType} from "../@types/event"; +import { normalize } from "../utils"; // These constants are used as sane defaults when the homeserver doesn't support // the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be @@ -58,6 +59,7 @@ function synthesizeReceipt(userId, event, receiptType) { return new MatrixEvent(fakeReceipt); } + /** * Construct a new Room. * @@ -101,6 +103,7 @@ function synthesizeReceipt(userId, event, receiptType) { * * @prop {string} roomId The ID of this room. * @prop {string} name The human-readable display name for this room. + * @prop {string} normalizedName The unhomoglyphed name for this room. * @prop {Array} timeline The live event timeline for this room, * with the oldest event at index 0. Present for backwards compatibility - * prefer getLiveTimeline().getEvents(). @@ -215,6 +218,10 @@ export function Room(roomId, client, myUserId, opts) { } else { this._membersPromise = null; } + + // flags to stop logspam about missing m.room.create events + this.getTypeWarning = false; + this.getVersionWarning = false; } /** @@ -227,6 +234,51 @@ function pendingEventsKey(roomId) { utils.inherits(Room, EventEmitter); + +/** + * Bulk decrypt critical events in a room + * + * Critical events represents the minimal set of events to decrypt + * for a typical UI to function properly + * + * - Last event of every room (to generate likely message preview) + * - All events up to the read receipt (to calculate an accurate notification count) + * + * @returns {Promise} Signals when all events have been decrypted + */ +Room.prototype.decryptCriticalEvents = function() { + const readReceiptEventId = this.getEventReadUpTo(this._client.getUserId(), true); + const events = this.getLiveTimeline().getEvents(); + const readReceiptTimelineIndex = events.findIndex(matrixEvent => { + return matrixEvent.event.event_id === readReceiptEventId; + }); + + const decryptionPromises = events + .slice(readReceiptTimelineIndex) + .filter(event => event.shouldAttemptDecryption()) + .reverse() + .map(event => event.attemptDecryption(this._client._crypto, { isRetry: true })); + + return Promise.allSettled(decryptionPromises); +}; + +/** + * Bulk decrypt events in a room + * + * @returns {Promise} Signals when all events have been decrypted + */ +Room.prototype.decryptAllEvents = function() { + const decryptionPromises = this + .getUnfilteredTimelineSet() + .getLiveTimeline() + .getEvents() + .filter(event => event.shouldAttemptDecryption()) + .reverse() + .map(event => event.attemptDecryption(this._client._crypto, { isRetry: true })); + + return Promise.allSettled(decryptionPromises); +}; + /** * Gets the version of the room * @returns {string} The version of the room, or null if it could not be determined @@ -234,7 +286,10 @@ utils.inherits(Room, EventEmitter); Room.prototype.getVersion = function() { const createEvent = this.currentState.getStateEvents("m.room.create", ""); if (!createEvent) { - logger.warn("[getVersion] Room " + this.roomId + " does not have an m.room.create event"); + if (!this.getVersionWarning) { + logger.warn("[getVersion] Room " + this.roomId + " does not have an m.room.create event"); + this.getVersionWarning = true; + } return '1'; } const ver = createEvent.getContent()['room_version']; @@ -440,6 +495,7 @@ Room.prototype.getLiveTimeline = function() { return this.getUnfilteredTimelineSet().getLiveTimeline(); }; + /** * Get the timestamp of the last message in the room * @@ -578,12 +634,13 @@ Room.prototype._loadMembersFromServer = async function() { at: lastSyncToken, }); const path = utils.encodeUri("/rooms/$roomId/members?" + queryString, - { $roomId: this.roomId }); + {$roomId: this.roomId}); const http = this._client._http; const response = await http.authedRequest(undefined, "GET", path); return response.chunk; }; + Room.prototype._loadMembers = async function() { // were the members loaded from the server? let fromServer = false; @@ -596,7 +653,7 @@ Room.prototype._loadMembers = async function() { `members from server for room ${this.roomId}`); } const memberEvents = rawMembersEvents.map(this._client.getEventMapper()); - return { memberEvents, fromServer }; + return {memberEvents, fromServer}; }; /** @@ -901,7 +958,7 @@ Room.prototype.getAliases = function() { if (aliasEvents) { for (let i = 0; i < aliasEvents.length; ++i) { const aliasEvent = aliasEvents[i]; - if (utils.isArray(aliasEvent.getContent().aliases)) { + if (Array.isArray(aliasEvent.getContent().aliases)) { const filteredAliases = aliasEvent.getContent().aliases.filter(a => { if (typeof(a) !== "string") return false; if (a[0] !== '#') return false; @@ -1029,7 +1086,7 @@ Room.prototype.getInvitedAndJoinedMemberCount = function() { * @return {RoomMember[]} A list of members with the given membership state. */ Room.prototype.getMembersWithMembership = function(membership) { - return utils.filter(this.currentState.getMembers(), function(m) { + return this.currentState.getMembers().filter(function(m) { return m.membership === membership; }); }; @@ -1068,6 +1125,7 @@ Room.prototype.getInvitedAndJoinedMemberCount = function() { return calculateRoomName(this, userId, true); }; + /** * Check if the given user_id has the given membership state. * @param {string} userId The user ID to check. @@ -1224,6 +1282,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy, fromCache) { } }; + /** * Add a pending outgoing event to this room. * @@ -1634,6 +1693,7 @@ Room.prototype.removeEvent = function(eventId) { return removedAny; }; + /** * Recalculate various aspects of the room, including the room name and * room summary. Call this any time the room's current state is modified. @@ -1649,7 +1709,7 @@ Room.prototype.recalculate = function() { ); if (membershipEvent && membershipEvent.getContent().membership === "invite") { const strippedStateEvents = membershipEvent.event.invite_room_state || []; - utils.forEach(strippedStateEvents, function(strippedEvent) { + strippedStateEvents.forEach(function(strippedEvent) { const existingEvent = self.currentState.getStateEvents( strippedEvent.type, strippedEvent.state_key, ); @@ -1669,6 +1729,7 @@ Room.prototype.recalculate = function() { const oldName = this.name; this.name = calculateRoomName(this, this.myUserId); + this.normalizedName = normalize(this.name); this.summary = new RoomSummary(this.roomId, { title: this.name, }); @@ -1799,9 +1860,9 @@ Room.prototype.addReceipt = function(event, fake) { */ Room.prototype._addReceiptsToStructure = function(event, receipts) { const self = this; - utils.keys(event.getContent()).forEach(function(eventId) { - utils.keys(event.getContent()[eventId]).forEach(function(receiptType) { - utils.keys(event.getContent()[eventId][receiptType]).forEach( + Object.keys(event.getContent()).forEach(function(eventId) { + Object.keys(event.getContent()[eventId]).forEach(function(receiptType) { + Object.keys(event.getContent()[eventId][receiptType]).forEach( function(userId) { const receipt = event.getContent()[eventId][receiptType][userId]; @@ -1841,8 +1902,8 @@ Room.prototype._addReceiptsToStructure = function(event, receipts) { */ Room.prototype._buildReceiptCache = function(receipts) { const receiptCacheByEventId = {}; - utils.keys(receipts).forEach(function(receiptType) { - utils.keys(receipts[receiptType]).forEach(function(userId) { + Object.keys(receipts).forEach(function(receiptType) { + Object.keys(receipts[receiptType]).forEach(function(userId) { const receipt = receipts[receiptType][userId]; if (!receiptCacheByEventId[receipt.eventId]) { receiptCacheByEventId[receipt.eventId] = []; @@ -1857,6 +1918,7 @@ Room.prototype._buildReceiptCache = function(receipts) { return receiptCacheByEventId; }; + /** * Add a temporary local-echo receipt to the room to reflect in the * client the fact that we've sent one. @@ -1914,6 +1976,7 @@ Room.prototype.getAccountData = function(type) { return this.accountData[type]; }; + /** * Returns whether the syncing user has permission to send a message in the room * @return {boolean} true if the user should be permitted to send @@ -1955,7 +2018,10 @@ Room.prototype.getJoinRule = function() { Room.prototype.getType = function() { const createEvent = this.currentState.getStateEvents("m.room.create", ""); if (!createEvent) { - logger.warn("[getType] Room " + this.roomId + " does not have an m.room.create event"); + if (!this.getTypeWarning) { + logger.warn("[getType] Room " + this.roomId + " does not have an m.room.create event"); + this.getTypeWarning = true; + } return undefined; } return createEvent.getContent()[RoomCreateTypeField]; diff --git a/src/models/search-result.js b/src/models/search-result.js index 667b6c88e..2250e786f 100644 --- a/src/models/search-result.js +++ b/src/models/search-result.js @@ -19,8 +19,7 @@ limitations under the License. * @module models/search-result */ -import * as utils from "../utils"; -import { EventContext } from "./event-context"; +import {EventContext} from "./event-context"; /** * Construct a new SearchResult @@ -52,8 +51,8 @@ SearchResult.fromJson = function(jsonObj, eventMapper) { const context = new EventContext(eventMapper(jsonObj.result)); context.setPaginateToken(jsonContext.start, true); - context.addEvents(utils.map(events_before, eventMapper), true); - context.addEvents(utils.map(events_after, eventMapper), false); + context.addEvents(events_before.map(eventMapper), true); + context.addEvents(events_after.map(eventMapper), false); context.setPaginateToken(jsonContext.end, false); return new SearchResult(jsonObj.rank, context); diff --git a/src/scheduler.js b/src/scheduler.js index 9ae68135e..37e231ce0 100644 --- a/src/scheduler.js +++ b/src/scheduler.js @@ -65,7 +65,7 @@ MatrixScheduler.prototype.getQueueForEvent = function(event) { if (!name || !this._queues[name]) { return null; } - return utils.map(this._queues[name], function(obj) { + return this._queues[name].map(function(obj) { return obj.event; }); }; @@ -195,16 +195,18 @@ function _startProcessingQueues(scheduler) { return; } // for each inactive queue with events in them - utils.forEach(utils.filter(utils.keys(scheduler._queues), function(queueName) { - return scheduler._activeQueues.indexOf(queueName) === -1 && - scheduler._queues[queueName].length > 0; - }), function(queueName) { - // mark the queue as active - scheduler._activeQueues.push(queueName); - // begin processing the head of the queue - debuglog("Spinning up queue: '%s'", queueName); - _processQueue(scheduler, queueName); - }); + Object.keys(scheduler._queues) + .filter(function(queueName) { + return scheduler._activeQueues.indexOf(queueName) === -1 && + scheduler._queues[queueName].length > 0; + }) + .forEach(function(queueName) { + // mark the queue as active + scheduler._activeQueues.push(queueName); + // begin processing the head of the queue + debuglog("Spinning up queue: '%s'", queueName); + _processQueue(scheduler, queueName); + }); } function _processQueue(scheduler, queueName) { @@ -266,7 +268,7 @@ function _processQueue(scheduler, queueName) { function _peekNextEvent(scheduler, queueName) { const queue = scheduler._queues[queueName]; - if (!utils.isArray(queue)) { + if (!Array.isArray(queue)) { return null; } return queue[0]; @@ -274,7 +276,7 @@ function _peekNextEvent(scheduler, queueName) { function _removeNextEvent(scheduler, queueName) { const queue = scheduler._queues[queueName]; - if (!utils.isArray(queue)) { + if (!Array.isArray(queue)) { return null; } return queue.shift(); diff --git a/src/store/memory.js b/src/store/memory.js index 673fc15d6..1b582ee43 100644 --- a/src/store/memory.js +++ b/src/store/memory.js @@ -22,8 +22,7 @@ limitations under the License. * @module store/memory */ -import { User } from "../models/user"; -import * as utils from "../utils"; +import {User} from "../models/user"; function isValidFilterId(filterId) { const isValidStr = typeof filterId === "string" && @@ -113,7 +112,7 @@ MemoryStore.prototype = { * @return {Group[]} A list of groups, which may be empty. */ getGroups: function() { - return utils.values(this.groups); + return Object.values(this.groups); }, /** @@ -175,7 +174,7 @@ MemoryStore.prototype = { * @return {Room[]} A list of rooms, which may be empty. */ getRooms: function() { - return utils.values(this.rooms); + return Object.values(this.rooms); }, /** @@ -194,7 +193,7 @@ MemoryStore.prototype = { * @return {RoomSummary[]} A summary of each room. */ getRoomSummaries: function() { - return utils.map(utils.values(this.rooms), function(room) { + return Object.values(this.rooms).map(function(room) { return room.summary; }); }, @@ -221,7 +220,7 @@ MemoryStore.prototype = { * @return {User[]} A list of users, which may be empty. */ getUsers: function() { - return utils.values(this.users); + return Object.values(this.users); }, /** diff --git a/src/sync.js b/src/sync.js index bf4dcd323..8ad4af30b 100644 --- a/src/sync.js +++ b/src/sync.js @@ -276,20 +276,15 @@ SyncApi.prototype.peek = function(roomId) { // FIXME: Mostly duplicated from _processRoomEvents but not entirely // because "state" in this API is at the BEGINNING of the chunk - const oldStateEvents = utils.map( - utils.deepCopy(response.state), client.getEventMapper(), - ); - const stateEvents = utils.map( - response.state, client.getEventMapper(), - ); - const messages = utils.map( - response.messages.chunk, client.getEventMapper(), - ); + const oldStateEvents = utils.deepCopy(response.state) + .map(client.getEventMapper()); + const stateEvents = response.state.map(client.getEventMapper()); + const messages = response.messages.chunk.map(client.getEventMapper()); // XXX: copypasted from /sync until we kill off this // minging v1 API stuff) // handle presence events (User objects) - if (response.presence && utils.isArray(response.presence)) { + if (response.presence && Array.isArray(response.presence)) { response.presence.map(client.getEventMapper()).forEach( function(presenceEvent) { let user = client.store.getUser(presenceEvent.getContent().user_id); @@ -1004,7 +999,7 @@ SyncApi.prototype._processSyncResponse = async function( // - The isBrandNewRoom boilerplate is boilerplatey. // handle presence events (User objects) - if (data.presence && utils.isArray(data.presence.events)) { + if (data.presence && Array.isArray(data.presence.events)) { data.presence.events.map(client.getEventMapper()).forEach( function(presenceEvent) { let user = client.store.getUser(presenceEvent.getSender()); @@ -1020,7 +1015,7 @@ SyncApi.prototype._processSyncResponse = async function( } // handle non-room account_data - if (data.account_data && utils.isArray(data.account_data.events)) { + if (data.account_data && Array.isArray(data.account_data.events)) { const events = data.account_data.events.map(client.getEventMapper()); const prevEventsMap = events.reduce((m, c) => { m[c.getId()] = client.store.getAccountData(c.getType()); @@ -1045,7 +1040,7 @@ SyncApi.prototype._processSyncResponse = async function( } // handle to-device events - if (data.to_device && utils.isArray(data.to_device.events) && + if (data.to_device && Array.isArray(data.to_device.events) && data.to_device.events.length > 0 ) { const cancelledKeyVerificationTxns = []; @@ -1156,10 +1151,15 @@ SyncApi.prototype._processSyncResponse = async function( await utils.promiseMapSeries(joinRooms, async function(joinObj) { const room = joinObj.room; const stateEvents = self._mapSyncEventsFormat(joinObj.state, room); - const timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room); + // Prevent events from being decrypted ahead of time + // this helps large account to speed up faster + // room::decryptCriticalEvent is in charge of decrypting all the events + // required for a client to function properly + const timelineEvents = self._mapSyncEventsFormat(joinObj.timeline, room, false); const ephemeralEvents = self._mapSyncEventsFormat(joinObj.ephemeral); const accountDataEvents = self._mapSyncEventsFormat(joinObj.account_data); + const encrypted = client.isRoomEncrypted(room.roomId); // we do this first so it's correct when any of the events fire if (joinObj.unread_notifications) { room.setUnreadNotificationCount( @@ -1170,7 +1170,6 @@ SyncApi.prototype._processSyncResponse = async function( // bother setting it here. We trust our calculations better than the // server's for this case, and therefore will assume that our non-zero // count is accurate. - const encrypted = client.isRoomEncrypted(room.roomId); if (!encrypted || (encrypted && room.getUnreadNotificationCount('highlight') <= 0)) { room.setUnreadNotificationCount( @@ -1292,6 +1291,11 @@ SyncApi.prototype._processSyncResponse = async function( }); room.updateMyMembership("join"); + + // Decrypt only the last message in all rooms to make sure we can generate a preview + // And decrypt all events after the recorded read receipt to ensure an accurate + // notification count + room.decryptCriticalEvents(); }); // Handle leaves (e.g. kicked rooms) @@ -1497,7 +1501,7 @@ SyncApi.prototype._mapSyncResponseToRoomArray = function(obj) { // [{stuff+Room+isBrandNewRoom}, {stuff+Room+isBrandNewRoom}] const client = this.client; const self = this; - return utils.keys(obj).map(function(roomId) { + return Object.keys(obj).map(function(roomId) { const arrObj = obj[roomId]; let room = client.store.getRoom(roomId); let isBrandNewRoom = false; @@ -1514,13 +1518,14 @@ SyncApi.prototype._mapSyncResponseToRoomArray = function(obj) { /** * @param {Object} obj * @param {Room} room + * @param {bool} decrypt * @return {MatrixEvent[]} */ -SyncApi.prototype._mapSyncEventsFormat = function(obj, room) { - if (!obj || !utils.isArray(obj.events)) { +SyncApi.prototype._mapSyncEventsFormat = function(obj, room, decrypt = true) { + if (!obj || !Array.isArray(obj.events)) { return []; } - const mapper = this.client.getEventMapper(); + const mapper = this.client.getEventMapper({ decrypt }); return obj.events.map(function(e) { if (room) { e.room_id = room.roomId; diff --git a/src/utils.ts b/src/utils.ts index 7c5700c5e..73e973a72 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -61,116 +61,6 @@ export function encodeUri(pathTemplate: string, return pathTemplate; } -/** - * Applies a map function to the given array. - * @param {Array} array The array to apply the function to. - * @param {Function} fn The function that will be invoked for each element in - * the array with the signature fn(element){...} - * @return {Array} A new array with the results of the function. - */ -export function map(array: T[], fn: (t: T) => S): S[] { - const results = new Array(array.length); - for (let i = 0; i < array.length; i++) { - results[i] = fn(array[i]); - } - return results; -} - -/** - * Applies a filter function to the given array. - * @param {Array} array The array to apply the function to. - * @param {Function} fn The function that will be invoked for each element in - * the array. It should return true to keep the element. The function signature - * looks like fn(element, index, array){...}. - * @return {Array} A new array with the results of the function. - */ -export function filter(array: T[], - fn: (t: T, i?: number, a?: T[]) => boolean): T[] { - const results: T[] = []; - for (let i = 0; i < array.length; i++) { - if (fn(array[i], i, array)) { - results.push(array[i]); - } - } - return results; -} - -/** - * Get the keys for an object. Same as Object.keys(). - * @param {Object} obj The object to get the keys for. - * @return {string[]} The keys of the object. - */ -export function keys(obj: object): string[] { - const result = []; - for (const key in obj) { - if (!obj.hasOwnProperty(key)) { - continue; - } - result.push(key); - } - return result; -} - -/** - * Get the values for an object. - * @param {Object} obj The object to get the values for. - * @return {Array<*>} The values of the object. - */ -export function values(obj: Record): T[] { - const result = []; - for (const key in obj) { - if (!obj.hasOwnProperty(key)) { - continue; - } - result.push(obj[key]); - } - return result; -} - -/** - * Invoke a function for each item in the array. - * @param {Array} array The array. - * @param {Function} fn The function to invoke for each element. Has the - * function signature fn(element, index). - */ -export function forEach(array: T[], fn: (t: T, i: number) => void) { - for (let i = 0; i < array.length; i++) { - fn(array[i], i); - } -} - -/** - * The findElement() method returns a value in the array, if an element in the array - * satisfies (returns true) the provided testing function. Otherwise undefined - * is returned. - * @param {Array} array The array. - * @param {Function} fn Function to execute on each value in the array, with the - * function signature fn(element, index, array) - * @param {boolean} reverse True to search in reverse order. - * @return {*} The first value in the array which returns true for - * the given function. - */ -export function findElement( - array: T[], - fn: (t: T, i?: number, a?: T[]) => boolean, - reverse?: boolean, -) { - let i; - if (reverse) { - for (i = array.length - 1; i >= 0; i--) { - if (fn(array[i], i, array)) { - return array[i]; - } - } - } else { - for (i = 0; i < array.length; i++) { - if (fn(array[i], i, array)) { - return array[i]; - } - } - } -} - /** * The removeElement() method removes the first element in the array that * satisfies (returns true) the provided testing function. @@ -217,16 +107,6 @@ export function isFunction(value: any) { return Object.prototype.toString.call(value) === "[object Function]"; } -/** - * Checks if the given thing is an array. - * @param {*} value The thing to check. - * @return {boolean} True if it is an array. - */ -export function isArray(value: any) { - return Array.isArray ? Array.isArray(value) : - Boolean(value && value.constructor === Array); -} - /** * Checks that the given object has the specified keys. * @param {Object} obj The object to check. @@ -380,207 +260,6 @@ export function extend(...restParams) { return target; } -/** - * Run polyfills to add Array.map and Array.filter if they are missing. - */ -export function runPolyfills() { - // Array.prototype.filter - // ======================================================== - // SOURCE: - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter - if (!Array.prototype.filter) { - // eslint-disable-next-line no-extend-native - Array.prototype.filter = function(fun: Function/*, thisArg*/, ...restProps) { - if (this === void 0 || this === null) { - throw new TypeError(); - } - - const t = Object(this); - const len = t.length >>> 0; - if (typeof fun !== 'function') { - throw new TypeError(); - } - - const res = []; - const thisArg = restProps ? restProps[0] : void 0; - for (let i = 0; i < len; i++) { - if (i in t) { - const val = t[i]; - - // NOTE: Technically this should Object.defineProperty at - // the next index, as push can be affected by - // properties on Object.prototype and Array.prototype. - // But that method's new, and collisions should be - // rare, so use the more-compatible alternative. - if (fun.call(thisArg, val, i, t)) { - res.push(val); - } - } - } - return res; - }; - } - - // Array.prototype.map - // ======================================================== - // SOURCE: - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map - // Production steps of ECMA-262, Edition 5, 15.4.4.19 - // Reference: http://es5.github.io/#x15.4.4.19 - if (!Array.prototype.map) { - // eslint-disable-next-line no-extend-native - Array.prototype.map = function(callback, thisArg) { - let T; - let k; - - if (this === null || this === undefined) { - throw new TypeError(' this is null or not defined'); - } - - // 1. Let O be the result of calling ToObject passing the |this| - // value as the argument. - const O = Object(this); - - // 2. Let lenValue be the result of calling the Get internal - // method of O with the argument "length". - // 3. Let len be ToUint32(lenValue). - const len = O.length >>> 0; - - // 4. If IsCallable(callback) is false, throw a TypeError exception. - // See: http://es5.github.com/#x9.11 - if (typeof callback !== 'function') { - throw new TypeError(callback + ' is not a function'); - } - - // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. - if (arguments.length > 1) { - T = thisArg; - } - - // 6. Let A be a new array created as if by the expression new Array(len) - // where Array is the standard built-in constructor with that name and - // len is the value of len. - const A = new Array(len); - - // 7. Let k be 0 - k = 0; - - // 8. Repeat, while k < len - while (k < len) { - let kValue; - let mappedValue; - - // a. Let Pk be ToString(k). - // This is implicit for LHS operands of the in operator - // b. Let kPresent be the result of calling the HasProperty internal - // method of O with argument Pk. - // This step can be combined with c - // c. If kPresent is true, then - if (k in O) { - // i. Let kValue be the result of calling the Get internal - // method of O with argument Pk. - kValue = O[k]; - - // ii. Let mappedValue be the result of calling the Call internal - // method of callback with T as the this value and argument - // list containing kValue, k, and O. - mappedValue = callback.call(T, kValue, k, O); - - // iii. Call the DefineOwnProperty internal method of A with arguments - // Pk, Property Descriptor - // { Value: mappedValue, - // Writable: true, - // Enumerable: true, - // Configurable: true }, - // and false. - - // In browsers that support Object.defineProperty, use the following: - // Object.defineProperty(A, k, { - // value: mappedValue, - // writable: true, - // enumerable: true, - // configurable: true - // }); - - // For best browser support, use the following: - A[k] = mappedValue; - } - // d. Increase k by 1. - k++; - } - - // 9. return A - return A; - }; - } - - // Array.prototype.forEach - // ======================================================== - // SOURCE: - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach - // Production steps of ECMA-262, Edition 5, 15.4.4.18 - // Reference: http://es5.github.io/#x15.4.4.18 - if (!Array.prototype.forEach) { - // eslint-disable-next-line no-extend-native - Array.prototype.forEach = function(callback, thisArg) { - let T; - let k; - - if (this === null || this === undefined) { - throw new TypeError(' this is null or not defined'); - } - - // 1. Let O be the result of calling ToObject passing the |this| value as the - // argument. - const O = Object(this); - - // 2. Let lenValue be the result of calling the Get internal method of O with the - // argument "length". - // 3. Let len be ToUint32(lenValue). - const len = O.length >>> 0; - - // 4. If IsCallable(callback) is false, throw a TypeError exception. - // See: http://es5.github.com/#x9.11 - if (typeof callback !== "function") { - throw new TypeError(callback + ' is not a function'); - } - - // 5. If thisArg was supplied, let T be thisArg; else let T be undefined. - if (arguments.length > 1) { - T = thisArg; - } - - // 6. Let k be 0 - k = 0; - - // 7. Repeat, while k < len - while (k < len) { - let kValue; - - // a. Let Pk be ToString(k). - // This is implicit for LHS operands of the in operator - // b. Let kPresent be the result of calling the HasProperty internal - // method of O with - // argument Pk. - // This step can be combined with c - // c. If kPresent is true, then - if (k in O) { - // i. Let kValue be the result of calling the Get internal method of O with - // argument Pk - kValue = O[k]; - - // ii. Call the Call internal method of callback with T as the this value and - // argument list containing kValue, k, and O. - callback.call(T, kValue, k, O); - } - // d. Increase k by 1. - k++; - } - // 8. return undefined - }; - } -} - /** * Inherit the prototype methods from one constructor into another. This is a * port of the Node.js implementation with an Object.create polyfill. @@ -667,6 +346,16 @@ export function removeHiddenChars(str: string): string { return ""; } +export function normalize(str: string): string { + // Note: we have to match the filter with the removeHiddenChars() because the + // function strips spaces and other characters (M becomes RN for example, in lowercase). + return removeHiddenChars(str.toLowerCase()) + // Strip all punctuation + .replace(/[\\'!"#$%&()*+,\-./:;<=>?@[\]^_`{|}~\u2000-\u206f\u2e00-\u2e7f]/g, "") + // We also doubly convert to lowercase to work around oddities of the library. + .toLowerCase(); +} + // Regex matching bunch of unicode control characters and otherwise misleading/invisible characters. // Includes: // various width spaces U+2000 - U+200D diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 090d5f74c..b6e650258 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -1505,7 +1505,8 @@ export class MatrixCall extends EventEmitter { } // Order is important here: first we stopAllMedia() and only then we can deleteAllFeeds() - this.stopAllMedia(); + // We don't stop media if the call was replaced as we want to re-use streams in the successor + if (hangupReason !== CallErrorCode.Replaced) this.stopAllMedia(); this.deleteAllFeeds(); this.hangupParty = hangupParty; @@ -1802,7 +1803,10 @@ export function createNewMatrixCall(client: any, roomId: string, options?: CallO window.RTCIceCandidate || navigator.mediaDevices, ); if (!supported) { - logger.error("WebRTC is not supported in this browser / environment"); + // Adds a lot of noise to test runs, so disable logging there. + if (process.env.NODE_ENV !== "test") { + logger.error("WebRTC is not supported in this browser / environment"); + } return null; } } catch (e) { diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 0d8b9f028..5bc5cb4cc 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -43,6 +43,9 @@ export class CallEventHandler { // after loading and after we've been offline for a bit. this.callEventBuffer = []; this.candidateEventsByCall = new Map>(); + } + + public start() { this.client.on("sync", this.evaluateEventBuffer); this.client.on("event", this.onEvent); } @@ -52,10 +55,11 @@ export class CallEventHandler { this.client.removeListener("event", this.onEvent); } - private evaluateEventBuffer = () => { + private evaluateEventBuffer = async () => { if (this.client.getSyncState() === "SYNCING") { - // don't process any events until they are all decrypted - if (this.callEventBuffer.some((e) => e.isBeingDecrypted())) return; + await Promise.all(this.callEventBuffer.map(event => { + this.client.decryptEventIfNeeded(event); + })); const ignoreCallIds = new Set(); // inspect the buffer and mark all calls which have been answered @@ -86,6 +90,7 @@ export class CallEventHandler { } private onEvent = (event: MatrixEvent) => { + this.client.decryptEventIfNeeded(event); // any call events or ones that might be once they're decrypted if ( event.getType().indexOf("m.call.") === 0 || diff --git a/yarn.lock b/yarn.lock index 9e97396d6..f48a13d6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1130,6 +1130,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz": + version "3.2.3" + resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4" + "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents": version "2.1.8-no-fsevents" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.tgz#da7c3996b8e6e19ebd14d82eaced2313e7769f9b" @@ -5142,10 +5146,6 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -"olm@https://packages.matrix.org/npm/olm/olm-3.2.1.tgz": - version "3.2.1" - resolved "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz#d623d76f99c3518dde68be8c86618d68bc7b004a" - once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"