From 867a0ca7eea1acbde1dca6f61ba624078b2fe277 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 21 Oct 2022 11:44:40 +0100 Subject: [PATCH] Apply more strict typescript around the codebase (#2778) * Apply more strict typescript around the codebase * Fix tests * Revert strict mode commit * Iterate strict * Iterate * Iterate strict * Iterate * Fix tests * Iterate * Iterate strict * Add tests * Iterate * Iterate * Fix tests * Fix tests * Strict types be strict * Fix types * detectOpenHandles * Strict * Fix client not stopping * Add sync peeking tests * Make test happier * More strict * Iterate * Stabilise * Moar strictness * Improve coverage * Fix types * Fix types * Improve types further * Fix types * Improve typing of NamespacedValue * Fix types --- spec/TestClient.ts | 26 +- spec/integ/matrix-client-crypto.spec.ts | 12 +- .../matrix-client-event-timeline.spec.ts | 2 +- spec/integ/matrix-client-methods.spec.ts | 42 +-- spec/integ/matrix-client-syncing.spec.ts | 61 ++++ spec/integ/megolm-integ.spec.ts | 16 +- spec/integ/sliding-sync-sdk.spec.ts | 3 +- spec/integ/sliding-sync.spec.ts | 32 +- spec/test-utils/webrtc.ts | 2 +- spec/unit/NamespacedValue.spec.ts | 16 +- spec/unit/autodiscovery.spec.ts | 2 +- spec/unit/content-repo.spec.ts | 16 + spec/unit/crypto.spec.ts | 197 +++++++----- spec/unit/crypto/CrossSigningInfo.spec.ts | 6 +- spec/unit/crypto/DeviceList.spec.ts | 2 +- spec/unit/crypto/algorithms/megolm.spec.ts | 22 +- spec/unit/crypto/algorithms/olm.spec.ts | 12 +- spec/unit/crypto/backup.spec.ts | 40 ++- spec/unit/crypto/cross-signing.spec.ts | 126 +++++--- spec/unit/crypto/crypto-utils.ts | 2 +- .../crypto/outgoing-room-key-requests.spec.ts | 2 +- spec/unit/crypto/secrets.spec.ts | 62 ++-- spec/unit/crypto/verification/request.spec.ts | 2 +- spec/unit/crypto/verification/sas.spec.ts | 97 +++--- spec/unit/crypto/verification/util.ts | 2 +- spec/unit/event-mapper.spec.ts | 2 +- spec/unit/event-timeline-set.spec.ts | 4 +- spec/unit/event-timeline.spec.ts | 2 +- spec/unit/filter.spec.ts | 16 + spec/unit/matrix-client.spec.ts | 156 +++++----- spec/unit/pushprocessor.spec.ts | 32 +- spec/unit/relations.spec.ts | 31 +- spec/unit/room.spec.ts | 22 +- spec/unit/timeline-window.spec.ts | 4 +- spec/unit/webrtc/call.spec.ts | 65 ++-- spec/unit/webrtc/callEventHandler.spec.ts | 2 +- src/NamespacedValue.ts | 17 +- src/ToDeviceMessageQueue.ts | 8 +- src/autodiscovery.ts | 2 +- src/client.ts | 278 ++++++++--------- src/crypto/CrossSigning.ts | 38 +-- src/crypto/DeviceList.ts | 85 +++--- src/crypto/OlmDevice.ts | 243 +++++++-------- src/crypto/SecretStorage.ts | 6 +- src/crypto/algorithms/base.ts | 12 +- src/crypto/algorithms/megolm.ts | 52 ++-- src/crypto/algorithms/olm.ts | 18 +- src/crypto/backup.ts | 130 ++++---- src/crypto/dehydration.ts | 28 +- src/crypto/index.ts | 173 +++++------ src/crypto/olmlib.ts | 24 +- src/crypto/store/base.ts | 4 +- .../store/indexeddb-crypto-store-backend.ts | 42 +-- src/crypto/store/indexeddb-crypto-store.ts | 97 +++--- src/crypto/store/localStorage-crypto-store.ts | 24 +- src/crypto/store/memory-crypto-store.ts | 2 +- src/crypto/verification/Base.ts | 32 +- src/crypto/verification/QRCode.ts | 73 +++-- .../request/VerificationRequest.ts | 56 ++-- src/errors.js | 52 ---- src/errors.ts | 51 ++++ src/event-mapper.ts | 4 +- src/http-api/errors.ts | 8 +- src/http-api/fetch.ts | 8 +- src/http-api/utils.ts | 4 +- src/logger.ts | 12 +- src/matrix.ts | 2 +- src/models/MSC3089TreeSpace.ts | 4 +- src/models/beacon.ts | 40 +-- src/models/event-timeline-set.ts | 2 +- src/models/event-timeline.ts | 17 +- src/models/event.ts | 19 +- src/models/relations-container.ts | 16 +- src/models/relations.ts | 32 +- src/models/room.ts | 2 +- src/models/thread.ts | 14 +- src/pushprocessor.ts | 22 +- src/realtime-callbacks.ts | 19 +- src/rendezvous/MSC3906Rendezvous.ts | 4 +- src/room-hierarchy.ts | 19 +- src/scheduler.ts | 17 +- src/sliding-sync-sdk.ts | 45 ++- src/sliding-sync.ts | 39 +-- src/store/index.ts | 2 +- src/store/memory.ts | 4 +- src/store/stub.ts | 8 +- src/sync-accumulator.ts | 18 +- src/sync.ts | 234 ++++++++------- src/timeline-window.ts | 38 ++- src/utils.ts | 4 +- src/webrtc/call.ts | 281 +++++++++--------- src/webrtc/callEventHandler.ts | 10 +- src/webrtc/callFeed.ts | 45 +-- src/webrtc/mediaHandler.ts | 15 +- 94 files changed, 1969 insertions(+), 1724 deletions(-) delete mode 100644 src/errors.js create mode 100644 src/errors.ts diff --git a/spec/TestClient.ts b/spec/TestClient.ts index 6056884dd31..249f5b39e16 100644 --- a/spec/TestClient.ts +++ b/spec/TestClient.ts @@ -38,8 +38,8 @@ import { IKeysUploadResponse, IUploadKeysRequest } from '../src/client'; export class TestClient { public readonly httpBackend: MockHttpBackend; public readonly client: MatrixClient; - public deviceKeys: IDeviceKeys; - public oneTimeKeys: Record; + public deviceKeys?: IDeviceKeys | null; + public oneTimeKeys?: Record; constructor( public readonly userId?: string, @@ -123,7 +123,7 @@ export class TestClient { logger.log(this + ': received device keys'); // we expect this to happen before any one-time keys are uploaded. - expect(Object.keys(this.oneTimeKeys).length).toEqual(0); + expect(Object.keys(this.oneTimeKeys!).length).toEqual(0); this.deviceKeys = content.device_keys; return { one_time_key_counts: { signed_curve25519: 0 } }; @@ -138,9 +138,9 @@ export class TestClient { * @returns {Promise} for the one-time keys */ public awaitOneTimeKeyUpload(): Promise> { - if (Object.keys(this.oneTimeKeys).length != 0) { + if (Object.keys(this.oneTimeKeys!).length != 0) { // already got one-time keys - return Promise.resolve(this.oneTimeKeys); + return Promise.resolve(this.oneTimeKeys!); } this.httpBackend.when("POST", "/keys/upload") @@ -148,7 +148,7 @@ export class TestClient { expect(content.device_keys).toBe(undefined); expect(content.one_time_keys).toBe(undefined); return { one_time_key_counts: { - signed_curve25519: Object.keys(this.oneTimeKeys).length, + signed_curve25519: Object.keys(this.oneTimeKeys!).length, } }; }); @@ -158,17 +158,17 @@ export class TestClient { expect(content.one_time_keys).toBeTruthy(); expect(content.one_time_keys).not.toEqual({}); logger.log('%s: received %i one-time keys', this, - Object.keys(content.one_time_keys).length); + Object.keys(content.one_time_keys!).length); this.oneTimeKeys = content.one_time_keys; return { one_time_key_counts: { - signed_curve25519: Object.keys(this.oneTimeKeys).length, + signed_curve25519: Object.keys(this.oneTimeKeys!).length, } }; }); // this can take ages return this.httpBackend.flush('/keys/upload', 2, 1000).then((flushed) => { expect(flushed).toEqual(2); - return this.oneTimeKeys; + return this.oneTimeKeys!; }); } @@ -183,7 +183,7 @@ export class TestClient { this.httpBackend.when('POST', '/keys/query').respond( 200, (_path, content) => { Object.keys(response.device_keys).forEach((userId) => { - expect(content.device_keys[userId]).toEqual([]); + expect(content.device_keys![userId]).toEqual([]); }); return response; }); @@ -206,7 +206,7 @@ export class TestClient { */ public getDeviceKey(): string { const keyId = 'curve25519:' + this.deviceId; - return this.deviceKeys.keys[keyId]; + return this.deviceKeys!.keys[keyId]; } /** @@ -216,7 +216,7 @@ export class TestClient { */ public getSigningKey(): string { const keyId = 'ed25519:' + this.deviceId; - return this.deviceKeys.keys[keyId]; + return this.deviceKeys!.keys[keyId]; } /** @@ -237,6 +237,6 @@ export class TestClient { } public getUserId(): string { - return this.userId; + return this.userId!; } } diff --git a/spec/integ/matrix-client-crypto.spec.ts b/spec/integ/matrix-client-crypto.spec.ts index f863949798a..38de34aa59d 100644 --- a/spec/integ/matrix-client-crypto.spec.ts +++ b/spec/integ/matrix-client-crypto.spec.ts @@ -59,7 +59,7 @@ async function bobUploadsDeviceKeys(): Promise { bobTestClient.client.uploadKeys(), bobTestClient.httpBackend.flushAllExpected(), ]); - expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0); + expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0); } /** @@ -99,7 +99,7 @@ async function expectAliClaimKeys(): Promise { expect(claimType).toEqual("signed_curve25519"); let keyId = ''; for (keyId in keys) { - if (bobTestClient.oneTimeKeys.hasOwnProperty(keyId)) { + if (bobTestClient.oneTimeKeys!.hasOwnProperty(keyId)) { if (keyId.indexOf(claimType + ":") === 0) { break; } @@ -137,7 +137,7 @@ async function aliDownloadsKeys(): Promise { // @ts-ignore - protected aliTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { const devices = data!.devices[bobUserId]!; - expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys.keys); + expect(devices[bobDeviceId].keys).toEqual(bobTestClient.deviceKeys!.keys); expect(devices[bobDeviceId].verified). toBe(DeviceInfo.DeviceVerification.UNVERIFIED); }); @@ -223,7 +223,7 @@ async function expectBobSendMessageRequest(): Promise { const content = await expectSendMessageRequest(bobTestClient.httpBackend); bobMessages.push(content); const aliKeyId = "curve25519:" + aliDeviceId; - const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId]; + const aliDeviceCurve25519Key = aliTestClient.deviceKeys!.keys[aliKeyId]; expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]); const ciphertext = content.ciphertext[aliDeviceCurve25519Key]; expect(ciphertext).toBeTruthy(); @@ -393,7 +393,7 @@ describe("MatrixClient crypto", () => { it("Ali gets keys with an invalid signature", async () => { await bobUploadsDeviceKeys(); // tamper bob's keys - const bobDeviceKeys = bobTestClient.deviceKeys; + const bobDeviceKeys = bobTestClient.deviceKeys!; expect(bobDeviceKeys.keys["curve25519:" + bobDeviceId]).toBeTruthy(); bobDeviceKeys.keys["curve25519:" + bobDeviceId] += "abc"; await Promise.all([ @@ -479,7 +479,7 @@ describe("MatrixClient crypto", () => { await bobTestClient.start(); const keys = await bobTestClient.awaitOneTimeKeyUpload(); expect(Object.keys(keys).length).toEqual(5); - expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0); + expect(Object.keys(bobTestClient.deviceKeys!).length).not.toEqual(0); }); it("Ali sends a message", async () => { diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index da089d76837..bc31b4d4e67 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -1047,7 +1047,7 @@ describe("MatrixClient event timelines", function() { response = { chunk: [THREAD_ROOT], state: [], - next_batch: RANDOM_TOKEN, + next_batch: RANDOM_TOKEN as string | null, }, ): ExpectedHttpRequest { const request = httpBackend.when("GET", encodeUri("/_matrix/client/r0/rooms/$roomId/threads", { diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index 273491e70fe..88366e2ca94 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -139,7 +139,7 @@ describe("MatrixClient", function() { const r = client!.cancelUpload(prom); expect(r).toBe(true); await expect(prom).rejects.toThrow("Aborted"); - expect(client.getCurrentUploads()).toHaveLength(0); + expect(client!.getCurrentUploads()).toHaveLength(0); }); }); @@ -178,7 +178,7 @@ describe("MatrixClient", function() { expect(request.data.third_party_signed).toEqual(signature); }).respond(200, { room_id: roomId }); - const prom = client.joinRoom(roomId, { + const prom = client!.joinRoom(roomId, { inviteSignUrl, viaServers, }); @@ -1164,18 +1164,18 @@ describe("MatrixClient", function() { describe("logout", () => { it("should abort pending requests when called with stopClient=true", async () => { - httpBackend.when("POST", "/logout").respond(200, {}); + httpBackend!.when("POST", "/logout").respond(200, {}); const fn = jest.fn(); - client.http.request(Method.Get, "/test").catch(fn); - client.logout(true); - await httpBackend.flush(undefined); + client!.http.request(Method.Get, "/test").catch(fn); + client!.logout(true); + await httpBackend!.flush(undefined); expect(fn).toHaveBeenCalled(); }); }); describe("sendHtmlEmote", () => { it("should send valid html emote", async () => { - httpBackend.when("PUT", "/send").check(req => { + httpBackend!.when("PUT", "/send").check(req => { expect(req.data).toStrictEqual({ "msgtype": "m.emote", "body": "Body", @@ -1184,15 +1184,15 @@ describe("MatrixClient", function() { "org.matrix.msc1767.message": expect.anything(), }); }).respond(200, { event_id: "$foobar" }); - const prom = client.sendHtmlEmote("!room:server", "Body", "

Body

"); - await httpBackend.flush(undefined); + const prom = client!.sendHtmlEmote("!room:server", "Body", "

Body

"); + await httpBackend!.flush(undefined); await expect(prom).resolves.toStrictEqual({ event_id: "$foobar" }); }); }); describe("sendHtmlMessage", () => { it("should send valid html message", async () => { - httpBackend.when("PUT", "/send").check(req => { + httpBackend!.when("PUT", "/send").check(req => { expect(req.data).toStrictEqual({ "msgtype": "m.text", "body": "Body", @@ -1201,24 +1201,24 @@ describe("MatrixClient", function() { "org.matrix.msc1767.message": expect.anything(), }); }).respond(200, { event_id: "$foobar" }); - const prom = client.sendHtmlMessage("!room:server", "Body", "

Body

"); - await httpBackend.flush(undefined); + const prom = client!.sendHtmlMessage("!room:server", "Body", "

Body

"); + await httpBackend!.flush(undefined); await expect(prom).resolves.toStrictEqual({ event_id: "$foobar" }); }); }); describe("forget", () => { it("should remove from store by default", async () => { - const room = new Room("!roomId:server", client, userId); - client.store.storeRoom(room); - expect(client.store.getRooms()).toContain(room); + const room = new Room("!roomId:server", client!, userId); + client!.store.storeRoom(room); + expect(client!.store.getRooms()).toContain(room); - httpBackend.when("POST", "/forget").respond(200, {}); + httpBackend!.when("POST", "/forget").respond(200, {}); await Promise.all([ - client.forget(room.roomId), - httpBackend.flushAllExpected(), + client!.forget(room.roomId), + httpBackend!.flushAllExpected(), ]); - expect(client.store.getRooms()).not.toContain(room); + expect(client!.store.getRooms()).not.toContain(room); }); }); @@ -1306,8 +1306,8 @@ describe("MatrixClient", function() { const resp = await prom; expect(resp.access_token).toBe(token); expect(resp.user_id).toBe(userId); - expect(client.getUserId()).toBe(userId); - expect(client.http.opts.accessToken).toBe(token); + expect(client!.getUserId()).toBe(userId); + expect(client!.http.opts.accessToken).toBe(token); }); }); diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index 9eab9507777..635c6fc94ed 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -1541,6 +1541,67 @@ describe("MatrixClient syncing", () => { }); }); + describe("peek", () => { + beforeEach(() => { + httpBackend!.expectedRequests = []; + }); + + it("should return a room based on the room initialSync API", async () => { + httpBackend!.when("GET", `/rooms/${encodeURIComponent(roomOne)}/initialSync`).respond(200, { + room_id: roomOne, + membership: "leave", + messages: { + start: "start", + end: "end", + chunk: [{ + content: { body: "Message 1" }, + type: "m.room.message", + event_id: "$eventId1", + sender: userA, + origin_server_ts: 12313525, + room_id: roomOne, + }, { + content: { body: "Message 2" }, + type: "m.room.message", + event_id: "$eventId2", + sender: userB, + origin_server_ts: 12315625, + room_id: roomOne, + }], + }, + state: [{ + content: { name: "Room Name" }, + type: "m.room.name", + event_id: "$eventId", + sender: userA, + origin_server_ts: 12314525, + state_key: "", + room_id: roomOne, + }], + presence: [{ + content: {}, + type: "m.presence", + sender: userA, + }], + }); + httpBackend!.when("GET", "/events").respond(200, { chunk: [] }); + + const prom = client!.peekInRoom(roomOne); + await httpBackend!.flushAllExpected(); + const room = await prom; + + expect(room.roomId).toBe(roomOne); + expect(room.getMyMembership()).toBe("leave"); + expect(room.name).toBe("Room Name"); + expect(room.currentState.getStateEvents("m.room.name", "").getId()).toBe("$eventId"); + expect(room.timeline[0].getContent().body).toBe("Message 1"); + expect(room.timeline[1].getContent().body).toBe("Message 2"); + client?.stopPeeking(); + httpBackend!.when("GET", "/events").respond(200, { chunk: [] }); + await httpBackend!.flushAllExpected(); + }); + }); + /** * waits for the MatrixClient to emit one or more 'sync' events. * diff --git a/spec/integ/megolm-integ.spec.ts b/spec/integ/megolm-integ.spec.ts index 6c42ccde4a2..a4891b702f6 100644 --- a/spec/integ/megolm-integ.spec.ts +++ b/spec/integ/megolm-integ.spec.ts @@ -1160,11 +1160,11 @@ describe("megolm", () => { "algorithm": 'm.megolm.v1.aes-sha2', "room_id": ROOM_ID, "sender_key": content.sender_key, - "sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key, + "sender_claimed_ed25519_key": groupSessionKey!.sender_claimed_ed25519_key, "session_id": content.session_id, - "session_key": groupSessionKey.key, - "chain_index": groupSessionKey.chain_index, - "forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain, + "session_key": groupSessionKey!.key, + "chain_index": groupSessionKey!.chain_index, + "forwarding_curve25519_key_chain": groupSessionKey!.forwarding_curve25519_key_chain, "org.matrix.msc3061.shared_history": true, }, plaintype: 'm.forwarded_room_key', @@ -1298,11 +1298,11 @@ describe("megolm", () => { "algorithm": 'm.megolm.v1.aes-sha2', "room_id": ROOM_ID, "sender_key": content.sender_key, - "sender_claimed_ed25519_key": groupSessionKey.sender_claimed_ed25519_key, + "sender_claimed_ed25519_key": groupSessionKey!.sender_claimed_ed25519_key, "session_id": content.session_id, - "session_key": groupSessionKey.key, - "chain_index": groupSessionKey.chain_index, - "forwarding_curve25519_key_chain": groupSessionKey.forwarding_curve25519_key_chain, + "session_key": groupSessionKey!.key, + "chain_index": groupSessionKey!.chain_index, + "forwarding_curve25519_key_chain": groupSessionKey!.forwarding_curve25519_key_chain, "org.matrix.msc3061.shared_history": true, }, plaintype: 'm.forwarded_room_key', diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index 3e50064a6d3..f09a9a3316c 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -468,7 +468,7 @@ describe("SlidingSyncSdk", () => { it("emits SyncState.Reconnecting when < FAILED_SYNC_ERROR_THRESHOLD & SyncState.Error when over", async () => { mockSlidingSync!.emit( SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, - { pos: "h", lists: [], rooms: {}, extensions: {} }, null, + { pos: "h", lists: [], rooms: {}, extensions: {} }, ); expect(sdk!.getSyncState()).toEqual(SyncState.Syncing); @@ -490,7 +490,6 @@ describe("SlidingSyncSdk", () => { SlidingSyncEvent.Lifecycle, SlidingSyncState.Complete, { pos: "i", lists: [], rooms: {}, extensions: {} }, - null, ); expect(sdk!.getSyncState()).toEqual(SyncState.Syncing); }); diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index 3390b48be94..5d5646a8db7 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -82,7 +82,7 @@ describe("SlidingSync", () => { it("should reset the connection on HTTP 400 and send everything again", async () => { // seed the connection with some lists, extensions and subscriptions to verify they are sent again - slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client, 1); + slidingSync = new SlidingSync(proxyBaseUrl, [], {}, client!, 1); const roomId = "!sub:localhost"; const subInfo = { timeline_limit: 42, @@ -108,7 +108,7 @@ describe("SlidingSync", () => { // expect everything to be sent let txnId; - httpBackend.when("POST", syncUrl).check(function(req) { + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); expect(body.room_subscriptions).toEqual({ @@ -117,7 +117,7 @@ describe("SlidingSync", () => { expect(body.lists[0]).toEqual(listInfo); expect(body.extensions).toBeTruthy(); expect(body.extensions["custom_extension"]).toEqual({ initial: true }); - expect(req.queryParams["pos"]).toBeUndefined(); + expect(req.queryParams!["pos"]).toBeUndefined(); txnId = body.txn_id; }).respond(200, function() { return { @@ -127,10 +127,10 @@ describe("SlidingSync", () => { txn_id: txnId, }; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); // expect nothing but ranges and non-initial extensions to be sent - httpBackend.when("POST", syncUrl).check(function(req) { + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); expect(body.room_subscriptions).toBeFalsy(); @@ -139,7 +139,7 @@ describe("SlidingSync", () => { }); expect(body.extensions).toBeTruthy(); expect(body.extensions["custom_extension"]).toEqual({ initial: false }); - expect(req.queryParams["pos"]).toEqual("11"); + expect(req.queryParams!["pos"]).toEqual("11"); }).respond(200, function() { return { pos: "12", @@ -147,19 +147,19 @@ describe("SlidingSync", () => { extensions: {}, }; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); // now we expire the session - httpBackend.when("POST", syncUrl).respond(400, function() { + httpBackend!.when("POST", syncUrl).respond(400, function() { logger.debug("sending session expired 400"); return { error: "HTTP 400 : session expired", }; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); // ...and everything should be sent again - httpBackend.when("POST", syncUrl).check(function(req) { + httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); expect(body.room_subscriptions).toEqual({ @@ -168,7 +168,7 @@ describe("SlidingSync", () => { expect(body.lists[0]).toEqual(listInfo); expect(body.extensions).toBeTruthy(); expect(body.extensions["custom_extension"]).toEqual({ initial: true }); - expect(req.queryParams["pos"]).toBeUndefined(); + expect(req.queryParams!["pos"]).toBeUndefined(); }).respond(200, function() { return { pos: "1", @@ -176,7 +176,7 @@ describe("SlidingSync", () => { extensions: {}, }; }); - await httpBackend.flushAllExpected(); + await httpBackend!.flushAllExpected(); slidingSync.stop(); }); }); @@ -415,7 +415,7 @@ describe("SlidingSync", () => { expect(slidingSync.getList(0)).toBeDefined(); expect(slidingSync.getList(5)).toBeNull(); expect(slidingSync.getListData(5)).toBeNull(); - const syncData = slidingSync.getListData(0); + const syncData = slidingSync.getListData(0)!; expect(syncData.joinedCount).toEqual(500); // from previous test expect(syncData.roomIndexToRoomId).toEqual({ 0: roomA, @@ -665,7 +665,7 @@ describe("SlidingSync", () => { 0: roomB, 1: roomC, }; - expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual(indexToRoomId); + expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual(indexToRoomId); httpBackend!.when("POST", syncUrl).respond(200, { pos: "f", // currently the list is [B,C] so we will insert D then immediately delete it @@ -703,7 +703,7 @@ describe("SlidingSync", () => { }); it("should handle deletions correctly", async () => { - expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({ + expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({ 0: roomB, 1: roomC, }); @@ -739,7 +739,7 @@ describe("SlidingSync", () => { }); it("should handle insertions correctly", async () => { - expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({ + expect(slidingSync.getListData(0)!.roomIndexToRoomId).toEqual({ 0: roomC, }); httpBackend!.when("POST", syncUrl).respond(200, { diff --git a/spec/test-utils/webrtc.ts b/spec/test-utils/webrtc.ts index f3404ebc5a2..0244e870c4b 100644 --- a/spec/test-utils/webrtc.ts +++ b/spec/test-utils/webrtc.ts @@ -135,7 +135,7 @@ export class MockMediaDeviceInfo { export class MockMediaHandler { getUserMediaStream(audio: boolean, video: boolean) { - const tracks = []; + const tracks: MockMediaStreamTrack[] = []; if (audio) tracks.push(new MockMediaStreamTrack("audio_track", "audio")); if (video) tracks.push(new MockMediaStreamTrack("video_track", "video")); diff --git a/spec/unit/NamespacedValue.spec.ts b/spec/unit/NamespacedValue.spec.ts index d2864a134b4..7ab2b03f285 100644 --- a/spec/unit/NamespacedValue.spec.ts +++ b/spec/unit/NamespacedValue.spec.ts @@ -32,7 +32,7 @@ describe("NamespacedValue", () => { }); it("should have a falsey unstable if needed", () => { - const ns = new NamespacedValue("stable", null); + const ns = new NamespacedValue("stable"); expect(ns.name).toBe(ns.stable); expect(ns.altName).toBeFalsy(); expect(ns.names).toEqual([ns.stable]); @@ -41,17 +41,17 @@ describe("NamespacedValue", () => { it("should match against either stable or unstable", () => { const ns = new NamespacedValue("stable", "unstable"); expect(ns.matches("no")).toBe(false); - expect(ns.matches(ns.stable)).toBe(true); - expect(ns.matches(ns.unstable)).toBe(true); + expect(ns.matches(ns.stable!)).toBe(true); + expect(ns.matches(ns.unstable!)).toBe(true); }); it("should not permit falsey values for both parts", () => { try { - new UnstableValue(null, null); + new UnstableValue(null!, null!); // noinspection ExceptionCaughtLocallyJS throw new Error("Failed to fail"); } catch (e) { - expect(e.message).toBe("One of stable or unstable values must be supplied"); + expect((e).message).toBe("One of stable or unstable values must be supplied"); } }); }); @@ -65,7 +65,7 @@ describe("UnstableValue", () => { }); it("should return unstable if there is no stable", () => { - const ns = new UnstableValue(null, "unstable"); + const ns = new UnstableValue(null!, "unstable"); expect(ns.name).toBe(ns.unstable); expect(ns.altName).toBeFalsy(); expect(ns.names).toEqual([ns.unstable]); @@ -73,11 +73,11 @@ describe("UnstableValue", () => { it("should not permit falsey unstable values", () => { try { - new UnstableValue("stable", null); + new UnstableValue("stable", null!); // noinspection ExceptionCaughtLocallyJS throw new Error("Failed to fail"); } catch (e) { - expect(e.message).toBe("Unstable value must be supplied"); + expect((e).message).toBe("Unstable value must be supplied"); } }); }); diff --git a/spec/unit/autodiscovery.spec.ts b/spec/unit/autodiscovery.spec.ts index 13688c25b25..fae5089c50b 100644 --- a/spec/unit/autodiscovery.spec.ts +++ b/spec/unit/autodiscovery.spec.ts @@ -678,7 +678,7 @@ describe("AutoDiscovery", function() { it("should return FAIL_PROMPT for connection errors", () => { const httpBackend = getHttpBackend(); - httpBackend.when("GET", "/.well-known/matrix/client").fail(0, undefined); + httpBackend.when("GET", "/.well-known/matrix/client").fail(0, undefined!); return Promise.all([ httpBackend.flushAllExpected(), AutoDiscovery.findClientConfig("example.org").then((conf) => { diff --git a/spec/unit/content-repo.spec.ts b/spec/unit/content-repo.spec.ts index 943d9f1ce83..27e7fbc9966 100644 --- a/spec/unit/content-repo.spec.ts +++ b/spec/unit/content-repo.spec.ts @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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 { getHttpUriForMxc } from "../../src/content-repo"; describe("ContentRepo", function() { diff --git a/spec/unit/crypto.spec.ts b/spec/unit/crypto.spec.ts index 6e46b3aaaa9..333868c78a6 100644 --- a/spec/unit/crypto.spec.ts +++ b/spec/unit/crypto.spec.ts @@ -2,6 +2,7 @@ import '../olm-loader'; // eslint-disable-next-line no-restricted-imports import { EventEmitter } from "events"; +import type { PkDecryption, PkSigning } from "@matrix-org/olm"; import { MatrixClient } from "../../src/client"; import { Crypto } from "../../src/crypto"; import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store"; @@ -32,7 +33,7 @@ function awaitEvent(emitter, event) { async function keyshareEventForEvent(client, event, index): Promise { const roomId = event.getRoomId(); const eventContent = event.getWireContent(); - const key = await client.crypto.olmDevice.getInboundGroupSessionKey( + const key = await client.crypto!.olmDevice.getInboundGroupSessionKey( roomId, eventContent.sender_key, eventContent.session_id, @@ -68,10 +69,10 @@ async function keyshareEventForEvent(client, event, index): Promise function roomKeyEventForEvent(client: MatrixClient, event: MatrixEvent): MatrixEvent { const roomId = event.getRoomId(); const eventContent = event.getWireContent(); - const key = client.crypto.olmDevice.getOutboundGroupSessionKey(eventContent.session_id); + const key = client.crypto!.olmDevice.getOutboundGroupSessionKey(eventContent.session_id); const ksEvent = new MatrixEvent({ type: "m.room_key", - sender: client.getUserId(), + sender: client.getUserId()!, content: { "algorithm": olmlib.MEGOLM_ALGORITHM, "room_id": roomId, @@ -146,7 +147,7 @@ describe("Crypto", function() { 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI'; device.keys["ed25519:FLIBBLE"] = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; - client.crypto.deviceList.getDeviceByIdentityKey = () => device; + client.crypto!.deviceList.getDeviceByIdentityKey = () => device; encryptionInfo = client.getEventEncryptionInfo(event); expect(encryptionInfo.encrypted).toBeTruthy(); @@ -334,7 +335,7 @@ describe("Crypto", function() { await Promise.all(events.map(async (event) => { // alice encrypts each event, and then bob tries to decrypt // them without any keys, so that they'll be in pending - await aliceClient.crypto.encryptEvent(event, aliceRoom); + await aliceClient.crypto!.encryptEvent(event, aliceRoom); // remove keys from the event // @ts-ignore private properties event.clearEvent = undefined; @@ -343,17 +344,17 @@ describe("Crypto", function() { // @ts-ignore private properties event.claimedEd25519Key = null; try { - await bobClient.crypto.decryptEvent(event); + await bobClient.crypto!.decryptEvent(event); } catch (e) { // we expect this to fail because we don't have the // decryption keys yet } })); - const device = new DeviceInfo(aliceClient.deviceId); - bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device; + const device = new DeviceInfo(aliceClient.deviceId!); + bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; - const bobDecryptor = bobClient.crypto.getRoomDecryptor( + const bobDecryptor = bobClient.crypto!.getRoomDecryptor( roomId, olmlib.MEGOLM_ALGORITHM, ); @@ -365,14 +366,14 @@ describe("Crypto", function() { // the first message can't be decrypted yet, but the second one // can let ksEvent = await keyshareEventForEvent(aliceClient, events[1], 1); - bobClient.crypto.deviceList.downloadKeys = () => Promise.resolve({}); - bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com"; + bobClient.crypto!.deviceList.downloadKeys = () => Promise.resolve({}); + bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com"; await bobDecryptor.onRoomKeyEvent(ksEvent); await decryptEventsPromise; expect(events[0].getContent().msgtype).toBe("m.bad.encrypted"); expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted"); - const cryptoStore = bobClient.crypto.cryptoStore; + const cryptoStore = bobClient.crypto!.cryptoStore; const eventContent = events[0].getWireContent(); const senderKey = eventContent.sender_key; const sessionId = eventContent.session_id; @@ -437,7 +438,7 @@ describe("Crypto", function() { }); // alice encrypts each event, and then bob tries to decrypt // them without any keys, so that they'll be in pending - await aliceClient.crypto.encryptEvent(event, aliceRoom); + await aliceClient.crypto!.encryptEvent(event, aliceRoom); // remove keys from the event // @ts-ignore private property event.clearEvent = undefined; @@ -446,24 +447,24 @@ describe("Crypto", function() { // @ts-ignore private property event.claimedEd25519Key = null; try { - await bobClient.crypto.decryptEvent(event); + await bobClient.crypto!.decryptEvent(event); } catch (e) { // we expect this to fail because we don't have the // decryption keys yet } - const device = new DeviceInfo(aliceClient.deviceId); - bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device; + const device = new DeviceInfo(aliceClient.deviceId!); + bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; - const bobDecryptor = bobClient.crypto.getRoomDecryptor( + const bobDecryptor = bobClient.crypto!.getRoomDecryptor( roomId, olmlib.MEGOLM_ALGORITHM, ); const ksEvent = await keyshareEventForEvent(aliceClient, event, 1); ksEvent.getContent().sender_key = undefined; // test - bobClient.crypto.olmDevice.addInboundGroupSession = jest.fn(); + bobClient.crypto!.olmDevice.addInboundGroupSession = jest.fn(); await bobDecryptor.onRoomKeyEvent(ksEvent); - expect(bobClient.crypto.olmDevice.addInboundGroupSession).not.toHaveBeenCalled(); + expect(bobClient.crypto!.olmDevice.addInboundGroupSession).not.toHaveBeenCalled(); }); it("creates a new keyshare request if we request a keyshare", async function() { @@ -479,7 +480,7 @@ describe("Crypto", function() { }, }); await aliceClient.cancelAndResendEventRoomKeyRequest(event); - const cryptoStore = aliceClient.crypto.cryptoStore; + const cryptoStore = aliceClient.crypto!.cryptoStore; const roomKeyRequestBody = { algorithm: olmlib.MEGOLM_ALGORITHM, room_id: "!someroom", @@ -514,7 +515,7 @@ describe("Crypto", function() { // let the client set up enough for that to happen, so gut-wrench a bit // to force it to send now. // @ts-ignore - aliceClient.crypto.outgoingRoomKeyRequestManager.sendQueuedRequests(); + aliceClient.crypto!.outgoingRoomKeyRequestManager.sendQueuedRequests(); jest.runAllTimers(); await Promise.resolve(); expect(aliceSendToDevice).toBeCalledTimes(1); @@ -571,7 +572,7 @@ describe("Crypto", function() { await Promise.all(events.map(async (event) => { // alice encrypts each event, and then bob tries to decrypt // them without any keys, so that they'll be in pending - await aliceClient.crypto.encryptEvent(event, aliceRoom); + await aliceClient.crypto!.encryptEvent(event, aliceRoom); // remove keys from the event // @ts-ignore private properties event.clearEvent = undefined; @@ -580,18 +581,18 @@ describe("Crypto", function() { // @ts-ignore private properties event.claimedEd25519Key = null; try { - await bobClient.crypto.decryptEvent(event); + await bobClient.crypto!.decryptEvent(event); } catch (e) { // we expect this to fail because we don't have the // decryption keys yet } })); - const device = new DeviceInfo(aliceClient.deviceId); - bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device; - bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com"; + const device = new DeviceInfo(aliceClient.deviceId!); + bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; + bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com"; - const cryptoStore = bobClient.crypto.cryptoStore; + const cryptoStore = bobClient.crypto!.cryptoStore; const eventContent = events[0].getWireContent(); const senderKey = eventContent.sender_key; const sessionId = eventContent.session_id; @@ -604,11 +605,11 @@ describe("Crypto", function() { const outgoingReq = await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody); expect(outgoingReq).toBeDefined(); await cryptoStore.updateOutgoingRoomKeyRequest( - outgoingReq.requestId, RoomKeyRequestState.Unsent, + outgoingReq!.requestId, RoomKeyRequestState.Unsent, { state: RoomKeyRequestState.Sent }, ); - const bobDecryptor = bobClient.crypto.getRoomDecryptor( + const bobDecryptor = bobClient.crypto!.getRoomDecryptor( roomId, olmlib.MEGOLM_ALGORITHM, ); @@ -617,7 +618,7 @@ describe("Crypto", function() { })); const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); await bobDecryptor.onRoomKeyEvent(ksEvent); - const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey( + const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey( roomId, events[0].getWireContent().sender_key, events[0].getWireContent().session_id, @@ -675,7 +676,7 @@ describe("Crypto", function() { await Promise.all(events.map(async (event) => { // alice encrypts each event, and then bob tries to decrypt // them without any keys, so that they'll be in pending - await aliceClient.crypto.encryptEvent(event, aliceRoom); + await aliceClient.crypto!.encryptEvent(event, aliceRoom); // remove keys from the event // @ts-ignore private properties event.clearEvent = undefined; @@ -684,18 +685,18 @@ describe("Crypto", function() { // @ts-ignore private properties event.claimedEd25519Key = null; try { - await bobClient.crypto.decryptEvent(event); + await bobClient.crypto!.decryptEvent(event); } catch (e) { // we expect this to fail because we don't have the // decryption keys yet } })); - const device = new DeviceInfo(claraClient.deviceId); - bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device; - bobClient.crypto.deviceList.getUserByIdentityKey = () => "@clara:example.com"; + const device = new DeviceInfo(claraClient.deviceId!); + bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; + bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@clara:example.com"; - const bobDecryptor = bobClient.crypto.getRoomDecryptor( + const bobDecryptor = bobClient.crypto!.getRoomDecryptor( roomId, olmlib.MEGOLM_ALGORITHM, ); @@ -703,10 +704,10 @@ describe("Crypto", function() { return awaitEvent(ev, "Event.decrypted"); })); const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); - ksEvent.event.sender = claraClient.getUserId(), - ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()); + ksEvent.event.sender = claraClient.getUserId()!; + ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()!); await bobDecryptor.onRoomKeyEvent(ksEvent); - const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey( + const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey( roomId, events[0].getWireContent().sender_key, events[0].getWireContent().session_id, @@ -753,7 +754,7 @@ describe("Crypto", function() { await Promise.all(events.map(async (event) => { // alice encrypts each event, and then bob tries to decrypt // them without any keys, so that they'll be in pending - await aliceClient.crypto.encryptEvent(event, aliceRoom); + await aliceClient.crypto!.encryptEvent(event, aliceRoom); // remove keys from the event // @ts-ignore private properties event.clearEvent = undefined; @@ -762,19 +763,19 @@ describe("Crypto", function() { // @ts-ignore private properties event.claimedEd25519Key = null; try { - await bobClient.crypto.decryptEvent(event); + await bobClient.crypto!.decryptEvent(event); } catch (e) { // we expect this to fail because we don't have the // decryption keys yet } })); - const device = new DeviceInfo(claraClient.deviceId); + const device = new DeviceInfo(claraClient.deviceId!); device.verified = DeviceInfo.DeviceVerification.VERIFIED; - bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device; - bobClient.crypto.deviceList.getUserByIdentityKey = () => "@bob:example.com"; + bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; + bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@bob:example.com"; - const bobDecryptor = bobClient.crypto.getRoomDecryptor( + const bobDecryptor = bobClient.crypto!.getRoomDecryptor( roomId, olmlib.MEGOLM_ALGORITHM, ); @@ -782,10 +783,10 @@ describe("Crypto", function() { return awaitEvent(ev, "Event.decrypted"); })); const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); - ksEvent.event.sender = bobClient.getUserId(), - ksEvent.sender = new RoomMember(roomId, bobClient.getUserId()); + ksEvent.event.sender = bobClient.getUserId()!; + ksEvent.sender = new RoomMember(roomId, bobClient.getUserId()!); await bobDecryptor.onRoomKeyEvent(ksEvent); - const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey( + const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey( roomId, events[0].getWireContent().sender_key, events[0].getWireContent().session_id, @@ -835,7 +836,7 @@ describe("Crypto", function() { await Promise.all(events.map(async (event) => { // alice encrypts each event, and then bob tries to decrypt // them without any keys, so that they'll be in pending - await aliceClient.crypto.encryptEvent(event, aliceRoom); + await aliceClient.crypto!.encryptEvent(event, aliceRoom); // remove keys from the event // @ts-ignore private properties event.clearEvent = undefined; @@ -844,26 +845,26 @@ describe("Crypto", function() { // @ts-ignore private properties event.claimedEd25519Key = null; try { - await bobClient.crypto.decryptEvent(event); + await bobClient.crypto!.decryptEvent(event); } catch (e) { // we expect this to fail because we don't have the // decryption keys yet } })); - const device = new DeviceInfo(claraClient.deviceId); - bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device; - bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com"; + const device = new DeviceInfo(claraClient.deviceId!); + bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; + bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com"; - const bobDecryptor = bobClient.crypto.getRoomDecryptor( + const bobDecryptor = bobClient.crypto!.getRoomDecryptor( roomId, olmlib.MEGOLM_ALGORITHM, ); const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); - ksEvent.event.sender = claraClient.getUserId(), - ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()); + ksEvent.event.sender = claraClient.getUserId()!; + ksEvent.sender = new RoomMember(roomId, claraClient.getUserId()!); await bobDecryptor.onRoomKeyEvent(ksEvent); - const key = await bobClient.crypto.olmDevice.getInboundGroupSessionKey( + const key = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey( roomId, events[0].getWireContent().sender_key, events[0].getWireContent().session_id, @@ -904,7 +905,7 @@ describe("Crypto", function() { await Promise.all(events.map(async (event) => { // alice encrypts each event, and then bob tries to decrypt // them without any keys, so that they'll be in pending - await aliceClient.crypto.encryptEvent(event, aliceRoom); + await aliceClient.crypto!.encryptEvent(event, aliceRoom); // remove keys from the event // @ts-ignore private properties event.clearEvent = undefined; @@ -914,11 +915,11 @@ describe("Crypto", function() { event.claimedEd25519Key = null; })); - const device = new DeviceInfo(aliceClient.deviceId); - bobClient.crypto.deviceList.getDeviceByIdentityKey = () => device; - bobClient.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com"; + const device = new DeviceInfo(aliceClient.deviceId!); + bobClient.crypto!.deviceList.getDeviceByIdentityKey = () => device; + bobClient.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com"; - const bobDecryptor = bobClient.crypto.getRoomDecryptor( + const bobDecryptor = bobClient.crypto!.getRoomDecryptor( roomId, olmlib.MEGOLM_ALGORITHM, ); @@ -926,25 +927,25 @@ describe("Crypto", function() { const ksEvent = await keyshareEventForEvent(aliceClient, events[0], 0); await bobDecryptor.onRoomKeyEvent(ksEvent); - const bobKey = await bobClient.crypto.olmDevice.getInboundGroupSessionKey( + const bobKey = await bobClient.crypto!.olmDevice.getInboundGroupSessionKey( roomId, content.sender_key, content.session_id, ); expect(bobKey).toBeNull(); - const aliceKey = await aliceClient.crypto.olmDevice.getInboundGroupSessionKey( + const aliceKey = await aliceClient.crypto!.olmDevice.getInboundGroupSessionKey( roomId, content.sender_key, content.session_id, ); - const parked = await bobClient.crypto.cryptoStore.takeParkedSharedHistory(roomId); + const parked = await bobClient.crypto!.cryptoStore.takeParkedSharedHistory(roomId); expect(parked).toEqual([{ senderId: aliceClient.getUserId(), senderKey: content.sender_key, sessionId: content.session_id, - sessionKey: aliceKey.key, - keysClaimed: { ed25519: aliceKey.sender_claimed_ed25519_key }, + sessionKey: aliceKey!.key, + keysClaimed: { ed25519: aliceKey!.sender_claimed_ed25519_key }, forwardingCurve25519KeyChain: ["akey"], }]); }); @@ -956,19 +957,19 @@ describe("Crypto", function() { jest.setTimeout(10000); const client = (new TestClient("@a:example.com", "dev")).client; await client.initCrypto(); - client.crypto.getSecretStorageKey = jest.fn().mockResolvedValue(null); - client.crypto.isCrossSigningReady = async () => false; - client.crypto.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null); - client.crypto.baseApis.setAccountData = jest.fn().mockResolvedValue(null); - client.crypto.baseApis.uploadKeySignatures = jest.fn(); - client.crypto.baseApis.http.authedRequest = jest.fn(); + client.crypto!.getSecretStorageKey = jest.fn().mockResolvedValue(null); + client.crypto!.isCrossSigningReady = async () => false; + client.crypto!.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null); + client.crypto!.baseApis.setAccountData = jest.fn().mockResolvedValue(null); + client.crypto!.baseApis.uploadKeySignatures = jest.fn(); + client.crypto!.baseApis.http.authedRequest = jest.fn(); const createSecretStorageKey = async () => { return { keyInfo: undefined, // Returning undefined here used to cause a crash privateKey: Uint8Array.of(32, 33), }; }; - await client.crypto.bootstrapSecretStorage({ + await client.crypto!.bootstrapSecretStorage({ createSecretStorageKey, }); client.stopClient(); @@ -995,7 +996,7 @@ describe("Crypto", function() { encryptedPayload = { algorithm: "m.olm.v1.curve25519-aes-sha2", - sender_key: client.client.crypto.olmDevice.deviceCurve25519Key, + sender_key: client.client.crypto!.olmDevice.deviceCurve25519Key, ciphertext: { plaintext: JSON.stringify(payload) }, }; }); @@ -1075,4 +1076,50 @@ describe("Crypto", function() { client.httpBackend.verifyNoOutstandingRequests(); }); }); + + describe("checkSecretStoragePrivateKey", () => { + let client: TestClient; + + beforeEach(async () => { + client = new TestClient("@alice:example.org", "aliceweb"); + await client.client.initCrypto(); + }); + + afterEach(async () => { + await client.stop(); + }); + + it("should free PkDecryption", () => { + const free = jest.fn(); + jest.spyOn(Olm, "PkDecryption").mockImplementation(() => ({ + init_with_private_key: jest.fn(), + free, + }) as unknown as PkDecryption); + client.client.checkSecretStoragePrivateKey(new Uint8Array(), ""); + expect(free).toHaveBeenCalled(); + }); + }); + + describe("checkCrossSigningPrivateKey", () => { + let client: TestClient; + + beforeEach(async () => { + client = new TestClient("@alice:example.org", "aliceweb"); + await client.client.initCrypto(); + }); + + afterEach(async () => { + await client.stop(); + }); + + it("should free PkSigning", () => { + const free = jest.fn(); + jest.spyOn(Olm, "PkSigning").mockImplementation(() => ({ + init_with_seed: jest.fn(), + free, + }) as unknown as PkSigning); + client.client.checkCrossSigningPrivateKey(new Uint8Array(), ""); + expect(free).toHaveBeenCalled(); + }); + }); }); diff --git a/spec/unit/crypto/CrossSigningInfo.spec.ts b/spec/unit/crypto/CrossSigningInfo.spec.ts index 9ed50a60c73..4bf29e31ed3 100644 --- a/spec/unit/crypto/CrossSigningInfo.spec.ts +++ b/spec/unit/crypto/CrossSigningInfo.spec.ts @@ -247,14 +247,14 @@ describe.each([ const olmDevice = new OlmDevice(store); const { getCrossSigningKeyCache, storeCrossSigningKeyCache } = createCryptoStoreCacheCallbacks(store, olmDevice); - await storeCrossSigningKeyCache("self_signing", testKey); + await storeCrossSigningKeyCache!("self_signing", testKey); // If we've not saved anything, don't expect anything // Definitely don't accidentally return the wrong key for the type - const nokey = await getCrossSigningKeyCache("self", ""); + const nokey = await getCrossSigningKeyCache!("self", ""); expect(nokey).toBeNull(); - const key = await getCrossSigningKeyCache("self_signing", ""); + const key = await getCrossSigningKeyCache!("self_signing", ""); expect(new Uint8Array(key)).toEqual(testKey); }); }); diff --git a/spec/unit/crypto/DeviceList.spec.ts b/spec/unit/crypto/DeviceList.spec.ts index cb7f0fb0fe8..448c92b28e2 100644 --- a/spec/unit/crypto/DeviceList.spec.ts +++ b/spec/unit/crypto/DeviceList.spec.ts @@ -90,7 +90,7 @@ const signedDeviceList2: IDownloadKeyResult = { describe('DeviceList', function() { let downloadSpy; let cryptoStore; - let deviceLists = []; + let deviceLists: DeviceList[] = []; beforeEach(function() { deviceLists = []; diff --git a/spec/unit/crypto/algorithms/megolm.spec.ts b/spec/unit/crypto/algorithms/megolm.spec.ts index 0cc9ccb9024..6dec0ab1559 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.ts +++ b/spec/unit/crypto/algorithms/megolm.spec.ts @@ -32,8 +32,8 @@ import { ClientEvent, MatrixClient, RoomMember } from '../../../../src'; import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo'; import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning'; -const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2'); -const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2'); +const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!; +const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!; const ROOM_ID = '!ROOM:ID'; @@ -331,7 +331,7 @@ describe("MegolmDecryption", function() { }, }, }); - mockBaseApis.sendToDevice.mockResolvedValue(undefined); + mockBaseApis.sendToDevice.mockResolvedValue({}); mockBaseApis.queueToDevice.mockResolvedValue(undefined); aliceDeviceInfo = { @@ -515,8 +515,8 @@ describe("MegolmDecryption", function() { bobdevice1: { algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], keys: { - "ed25519:Dynabook": bobDevice1.deviceEd25519Key, - "curve25519:Dynabook": bobDevice1.deviceCurve25519Key, + "ed25519:Dynabook": bobDevice1.deviceEd25519Key!, + "curve25519:Dynabook": bobDevice1.deviceCurve25519Key!, }, verified: 0, known: false, @@ -524,8 +524,8 @@ describe("MegolmDecryption", function() { bobdevice2: { algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], keys: { - "ed25519:Dynabook": bobDevice2.deviceEd25519Key, - "curve25519:Dynabook": bobDevice2.deviceCurve25519Key, + "ed25519:Dynabook": bobDevice2.deviceEd25519Key!, + "curve25519:Dynabook": bobDevice2.deviceCurve25519Key!, }, verified: -1, known: false, @@ -614,8 +614,8 @@ describe("MegolmDecryption", function() { bobdevice: { algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], keys: { - "ed25519:bobdevice": bobDevice.deviceEd25519Key, - "curve25519:bobdevice": bobDevice.deviceCurve25519Key, + "ed25519:bobdevice": bobDevice.deviceEd25519Key!, + "curve25519:bobdevice": bobDevice.deviceCurve25519Key!, }, verified: 0, known: true, @@ -718,8 +718,8 @@ describe("MegolmDecryption", function() { device_id: "bobdevice", algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], keys: { - "ed25519:bobdevice": bobDevice.deviceEd25519Key, - "curve25519:bobdevice": bobDevice.deviceCurve25519Key, + "ed25519:bobdevice": bobDevice.deviceEd25519Key!, + "curve25519:bobdevice": bobDevice.deviceCurve25519Key!, }, known: true, verified: 1, diff --git a/spec/unit/crypto/algorithms/olm.spec.ts b/spec/unit/crypto/algorithms/olm.spec.ts index 7ed0c34f4be..b24532091ae 100644 --- a/spec/unit/crypto/algorithms/olm.spec.ts +++ b/spec/unit/crypto/algorithms/olm.spec.ts @@ -67,13 +67,13 @@ describe("OlmDevice", function() { const sid = await setupSession(aliceOlmDevice, bobOlmDevice); const ciphertext = await aliceOlmDevice.encryptMessage( - bobOlmDevice.deviceCurve25519Key, + bobOlmDevice.deviceCurve25519Key!, sid, "The olm or proteus is an aquatic salamander in the family Proteidae", ) as any; // OlmDevice.encryptMessage has incorrect return type const result = await bobOlmDevice.createInboundSession( - aliceOlmDevice.deviceCurve25519Key, + aliceOlmDevice.deviceCurve25519Key!, ciphertext.type, ciphertext.body, ); @@ -94,7 +94,7 @@ describe("OlmDevice", function() { + " in the family Proteidae" ); const ciphertext = await aliceOlmDevice.encryptMessage( - bobOlmDevice.deviceCurve25519Key, + bobOlmDevice.deviceCurve25519Key!, sessionId, MESSAGE, ) as any; // OlmDevice.encryptMessage has incorrect return type @@ -103,7 +103,7 @@ describe("OlmDevice", function() { bobRecreatedOlmDevice.init({ fromExportedDevice: exported }); const decrypted = await bobRecreatedOlmDevice.createInboundSession( - aliceOlmDevice.deviceCurve25519Key, + aliceOlmDevice.deviceCurve25519Key!, ciphertext.type, ciphertext.body, ); @@ -118,7 +118,7 @@ describe("OlmDevice", function() { + " the olm is entirely aquatic" ); const ciphertext2 = await aliceOlmDevice.encryptMessage( - bobOlmDevice.deviceCurve25519Key, + bobOlmDevice.deviceCurve25519Key!, sessionId, MESSAGE_2, ) as any; // OlmDevice.encryptMessage has incorrect return type @@ -128,7 +128,7 @@ describe("OlmDevice", function() { // Note: "decrypted_2" does not have the same structure as "decrypted" const decrypted2 = await bobRecreatedAgainOlmDevice.decryptMessage( - aliceOlmDevice.deviceCurve25519Key, + aliceOlmDevice.deviceCurve25519Key!, decrypted.session_id, ciphertext2.type, ciphertext2.body, diff --git a/spec/unit/crypto/backup.spec.ts b/spec/unit/crypto/backup.spec.ts index ca4c09c532c..0d0820cd3f8 100644 --- a/spec/unit/crypto/backup.spec.ts +++ b/spec/unit/crypto/backup.spec.ts @@ -34,7 +34,7 @@ import { MatrixScheduler } from '../../../src'; const Olm = global.Olm; -const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2'); +const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!; const ROOM_ID = '!ROOM:ID'; @@ -197,7 +197,7 @@ describe("MegolmBackup", function() { // to tick the clock between the first try and the retry. const realSetTimeout = global.setTimeout; jest.spyOn(global, 'setTimeout').mockImplementation(function(f, n) { - return realSetTimeout(f!, n/100); + return realSetTimeout(f!, n!/100); }); }); @@ -318,7 +318,7 @@ describe("MegolmBackup", function() { resolve(); return Promise.resolve({} as T); }; - client.crypto.backupManager.backupGroupSession( + client.crypto!.backupManager.backupGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", groupSession.session_id(), ); @@ -349,7 +349,7 @@ describe("MegolmBackup", function() { return client.initCrypto() .then(() => { - return client.crypto.storeSessionBackupPrivateKey(new Uint8Array(32)); + return client.crypto!.storeSessionBackupPrivateKey(new Uint8Array(32)); }) .then(() => { return cryptoStore.doTxn( @@ -401,7 +401,7 @@ describe("MegolmBackup", function() { resolve(); return Promise.resolve({} as T); }; - client.crypto.backupManager.backupGroupSession( + client.crypto!.backupManager.backupGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", groupSession.session_id(), ); @@ -449,7 +449,7 @@ describe("MegolmBackup", function() { try { // make sure auth_data is signed by the master key olmlib.pkVerify( - (data as Record).auth_data, client.getCrossSigningId(), "@alice:bar", + (data as Record).auth_data, client.getCrossSigningId()!, "@alice:bar", ); } catch (e) { reject(e); @@ -568,7 +568,7 @@ describe("MegolmBackup", function() { ); } }; - return client.crypto.backupManager.backupGroupSession( + return client.crypto!.backupManager.backupGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", groupSession.session_id(), ); @@ -699,4 +699,30 @@ describe("MegolmBackup", function() { )).rejects.toThrow(); }); }); + + describe("flagAllGroupSessionsForBackup", () => { + it("should return number of sesions needing backup", async () => { + const scheduler = [ + "getQueueForEvent", "queueEvent", "removeEventFromQueue", + "setProcessFunction", + ].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject; + const store = new StubStore(); + const client = new MatrixClient({ + baseUrl: "https://my.home.server", + idBaseUrl: "https://identity.server", + accessToken: "my.access.token", + fetchFn: jest.fn(), // NOP + store, + scheduler, + userId: "@alice:bar", + deviceId: "device", + cryptoStore, + }); + await client.initCrypto(); + + cryptoStore.countSessionsNeedingBackup = jest.fn().mockReturnValue(6); + await expect(client.flagAllGroupSessionsForBackup()).resolves.toBe(6); + client.stopClient(); + }); + }); }); diff --git a/spec/unit/crypto/cross-signing.spec.ts b/spec/unit/crypto/cross-signing.spec.ts index bfa7625cbe8..e9c112c5005 100644 --- a/spec/unit/crypto/cross-signing.spec.ts +++ b/spec/unit/crypto/cross-signing.spec.ts @@ -93,8 +93,8 @@ describe("Cross Signing", function() { ); alice.uploadDeviceSigningKeys = jest.fn().mockImplementation(async (auth, keys) => { await olmlib.verifySignature( - alice.crypto.olmDevice, keys.master_key, "@alice:example.com", - "Osborne2", alice.crypto.olmDevice.deviceEd25519Key, + alice.crypto!.olmDevice, keys.master_key, "@alice:example.com", + "Osborne2", alice.crypto!.olmDevice.deviceEd25519Key!, ); }); alice.uploadKeySignatures = async () => ({ failures: {} }); @@ -152,7 +152,7 @@ describe("Cross Signing", function() { authUploadDeviceSigningKeys, }); } catch (e) { - if (e.errcode === "M_FORBIDDEN") { + if ((e).errcode === "M_FORBIDDEN") { bootstrapDidThrow = true; } } @@ -169,7 +169,7 @@ describe("Cross Signing", function() { // set Alice's cross-signing key await resetCrossSigningKeys(alice); // Alice downloads Bob's device key - alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -238,12 +238,12 @@ describe("Cross Signing", function() { alice.uploadKeySignatures = jest.fn().mockImplementation(async (content) => { try { await olmlib.verifySignature( - alice.crypto.olmDevice, + alice.crypto!.olmDevice, content["@alice:example.com"][ "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk" ], "@alice:example.com", - "Osborne2", alice.crypto.olmDevice.deviceEd25519Key, + "Osborne2", alice.crypto!.olmDevice.deviceEd25519Key!, ); olmlib.pkVerify( content["@alice:example.com"]["Osborne2"], @@ -258,7 +258,7 @@ describe("Cross Signing", function() { }); // @ts-ignore private property - const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"] + const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"] .Osborne2; const aliceDevice = { user_id: "@alice:example.com", @@ -266,7 +266,7 @@ describe("Cross Signing", function() { keys: deviceInfo.keys, algorithms: deviceInfo.algorithms, }; - await alice.crypto.signObject(aliceDevice); + await alice.crypto!.signObject(aliceDevice); olmlib.pkSign( aliceDevice as ISignedKey, selfSigningKey as unknown as PkSigning, @@ -401,7 +401,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobMasterPubkey]: sskSig, }, }; - alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -435,7 +435,7 @@ describe("Cross Signing", function() { verified: 0, known: false, }; - alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice, }); // Bob's device key should be TOFU @@ -467,11 +467,11 @@ describe("Cross Signing", function() { const aliceKeys: Record = {}; const { client: alice, httpBackend } = await makeTestClient( { userId: "@alice:example.com", deviceId: "Osborne2" }, - null, + undefined, aliceKeys, ); - alice.crypto.deviceList.startTrackingDeviceList("@bob:example.com"); - alice.crypto.deviceList.stopTrackingAllDeviceLists = () => {}; + alice.crypto!.deviceList.startTrackingDeviceList("@bob:example.com"); + alice.crypto!.deviceList.stopTrackingAllDeviceLists = () => {}; alice.uploadDeviceSigningKeys = async () => ({}); alice.uploadKeySignatures = async () => ({ failures: {} }); @@ -486,7 +486,7 @@ describe("Cross Signing", function() { ]); const keyChangePromise = new Promise((resolve, reject) => { - alice.crypto.deviceList.once(CryptoEvent.UserCrossSigningUpdated, (userId) => { + alice.crypto!.deviceList.once(CryptoEvent.UserCrossSigningUpdated, (userId) => { if (userId === "@bob:example.com") { resolve(); } @@ -494,7 +494,7 @@ describe("Cross Signing", function() { }); // @ts-ignore private property - const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"] + const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"] .Osborne2; const aliceDevice = { user_id: "@alice:example.com", @@ -502,7 +502,7 @@ describe("Cross Signing", function() { keys: deviceInfo.keys, algorithms: deviceInfo.algorithms, }; - await alice.crypto.signObject(aliceDevice); + await alice.crypto!.signObject(aliceDevice); const bobOlmAccount = new global.Olm.Account(); bobOlmAccount.create(); @@ -667,7 +667,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobMasterPubkey]: sskSig, }, }; - alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -690,7 +690,7 @@ describe("Cross Signing", function() { "ed25519:Dynabook": "someOtherPubkey", }, }; - alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice as unknown as IDevice, }); // Bob's device key should be untrusted @@ -735,7 +735,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobMasterPubkey]: sskSig, }, }; - alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -770,7 +770,7 @@ describe("Cross Signing", function() { }, }, }; - alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice, }); // Alice verifies Bob's SSK @@ -802,7 +802,7 @@ describe("Cross Signing", function() { ["ed25519:" + bobMasterPubkey2]: sskSig2, }, }; - alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", { + alice.crypto!.deviceList.storeCrossSigningForUser("@bob:example.com", { keys: { master: { user_id: "@bob:example.com", @@ -838,8 +838,8 @@ describe("Cross Signing", function() { // Alice gets new signature for device const sig2 = bobSigning2.sign(bobDeviceString); - bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2; - alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { + bobDevice.signatures!["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2; + alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: bobDevice, }); @@ -876,20 +876,20 @@ describe("Cross Signing", function() { bob.uploadKeySignatures = async () => ({ failures: {} }); // set Bob's cross-signing key await resetCrossSigningKeys(bob); - alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", { + alice.crypto!.deviceList.storeDevicesForUser("@bob:example.com", { Dynabook: { algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"], keys: { - "curve25519:Dynabook": bob.crypto.olmDevice.deviceCurve25519Key, - "ed25519:Dynabook": bob.crypto.olmDevice.deviceEd25519Key, + "curve25519:Dynabook": bob.crypto!.olmDevice.deviceCurve25519Key!, + "ed25519:Dynabook": bob.crypto!.olmDevice.deviceEd25519Key!, }, verified: 1, known: true, }, }); - alice.crypto.deviceList.storeCrossSigningForUser( + alice.crypto!.deviceList.storeCrossSigningForUser( "@bob:example.com", - bob.crypto.crossSigningInfo.toStorage(), + bob.crypto!.crossSigningInfo.toStorage(), ); alice.uploadDeviceSigningKeys = async () => ({}); @@ -909,8 +909,8 @@ describe("Cross Signing", function() { expect(bobTrust.isTofu()).toBeTruthy(); // "forget" that Bob is trusted - delete alice.crypto.deviceList.crossSigningInfo["@bob:example.com"] - .keys.master.signatures["@alice:example.com"]; + delete alice.crypto!.deviceList.crossSigningInfo["@bob:example.com"] + .keys.master.signatures!["@alice:example.com"]; const bobTrust2 = alice.checkUserTrust("@bob:example.com"); expect(bobTrust2.isCrossSigningVerified()).toBeFalsy(); @@ -919,9 +919,9 @@ describe("Cross Signing", function() { upgradePromise = new Promise((resolve) => { upgradeResolveFunc = resolve; }); - alice.crypto.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com"); + alice.crypto!.deviceList.emit(CryptoEvent.UserCrossSigningUpdated, "@bob:example.com"); await new Promise((resolve) => { - alice.crypto.on(CryptoEvent.UserTrustStatusChanged, resolve); + alice.crypto!.on(CryptoEvent.UserTrustStatusChanged, resolve); }); await upgradePromise; @@ -963,7 +963,7 @@ describe("Cross Signing", function() { }; // Alice's device downloads the keys, but doesn't trust them yet - alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", { + alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", { keys: { master: { user_id: "@alice:example.com", @@ -999,7 +999,7 @@ describe("Cross Signing", function() { ["ed25519:" + alicePubkey]: sig, }, } }; - alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", { + alice.crypto!.deviceList.storeDevicesForUser("@alice:example.com", { [aliceDeviceId]: aliceCrossSignedDevice, }); @@ -1042,7 +1042,7 @@ describe("Cross Signing", function() { }; // Alice's device downloads the keys - alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", { + alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", { keys: { master: { user_id: "@alice:example.com", @@ -1067,11 +1067,65 @@ describe("Cross Signing", function() { "ed25519:Dynabook": "someOtherPubkey", }, }; - alice.crypto.deviceList.storeDevicesForUser("@alice:example.com", { + alice.crypto!.deviceList.storeDevicesForUser("@alice:example.com", { [deviceId]: aliceNotCrossSignedDevice, }); expect(alice.checkIfOwnDeviceCrossSigned(deviceId)).toBeFalsy(); alice.stopClient(); }); + + it("checkIfOwnDeviceCrossSigned should sanely handle unknown devices", async () => { + const { client: alice } = await makeTestClient( + { userId: "@alice:example.com", deviceId: "Osborne2" }, + ); + alice.uploadDeviceSigningKeys = async () => ({}); + alice.uploadKeySignatures = async () => ({ failures: {} }); + + // Generate Alice's SSK etc + const aliceMasterSigning = new global.Olm.PkSigning(); + const aliceMasterPrivkey = aliceMasterSigning.generate_seed(); + const aliceMasterPubkey = aliceMasterSigning.init_with_seed(aliceMasterPrivkey); + const aliceSigning = new global.Olm.PkSigning(); + const alicePrivkey = aliceSigning.generate_seed(); + const alicePubkey = aliceSigning.init_with_seed(alicePrivkey); + const aliceSSK: ICrossSigningKey = { + user_id: "@alice:example.com", + usage: ["self_signing"], + keys: { + ["ed25519:" + alicePubkey]: alicePubkey, + }, + }; + const sskSig = aliceMasterSigning.sign(anotherjson.stringify(aliceSSK)); + aliceSSK.signatures = { + "@alice:example.com": { + ["ed25519:" + aliceMasterPubkey]: sskSig, + }, + }; + + // Alice's device downloads the keys + alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", { + keys: { + master: { + user_id: "@alice:example.com", + usage: ["master"], + keys: { + ["ed25519:" + aliceMasterPubkey]: aliceMasterPubkey, + }, + }, + self_signing: aliceSSK, + }, + firstUse: true, + crossSigningVerifiedBefore: false, + }); + + expect(alice.checkIfOwnDeviceCrossSigned("notadevice")).toBeFalsy(); + alice.stopClient(); + }); + + it("checkIfOwnDeviceCrossSigned should sanely handle unknown users", async () => { + const { client: alice } = await makeTestClient({ userId: "@alice:example.com", deviceId: "Osborne2" }); + expect(alice.checkIfOwnDeviceCrossSigned("notadevice")).toBeFalsy(); + alice.stopClient(); + }); }); diff --git a/spec/unit/crypto/crypto-utils.ts b/spec/unit/crypto/crypto-utils.ts index 3535edaabe7..1391d79f193 100644 --- a/spec/unit/crypto/crypto-utils.ts +++ b/spec/unit/crypto/crypto-utils.ts @@ -39,7 +39,7 @@ export async function createSecretStorageKey(): Promise { decryption.free(); return { // `pubkey` not used anymore with symmetric 4S - keyInfo: { pubkey: storagePublicKey, key: undefined }, + keyInfo: { pubkey: storagePublicKey, key: undefined! }, privateKey: storagePrivateKey, }; } diff --git a/spec/unit/crypto/outgoing-room-key-requests.spec.ts b/spec/unit/crypto/outgoing-room-key-requests.spec.ts index 1b1a4f57a7d..049b7b7d365 100644 --- a/spec/unit/crypto/outgoing-room-key-requests.spec.ts +++ b/spec/unit/crypto/outgoing-room-key-requests.spec.ts @@ -93,7 +93,7 @@ describe.each([ await store.getOutgoingRoomKeyRequestByState([RoomKeyRequestState.Sent]); expect(r).not.toBeNull(); expect(r).not.toBeUndefined(); - expect(r.state).toEqual(RoomKeyRequestState.Sent); + expect(r!.state).toEqual(RoomKeyRequestState.Sent); expect(requests).toContainEqual(r); }); }); diff --git a/spec/unit/crypto/secrets.spec.ts b/spec/unit/crypto/secrets.spec.ts index ddac48cffb4..94293697f1b 100644 --- a/spec/unit/crypto/secrets.spec.ts +++ b/spec/unit/crypto/secrets.spec.ts @@ -21,9 +21,9 @@ import { MatrixEvent } from "../../../src/models/event"; import { TestClient } from '../../TestClient'; import { makeTestClients } from './verification/util'; import { encryptAES } from "../../../src/crypto/aes"; -import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils"; +import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils"; import { logger } from '../../../src/logger'; -import { ICreateClientOpts } from '../../../src/client'; +import { ClientEvent, ICreateClientOpts } from '../../../src/client'; import { ISecretStorageKeyInfo } from '../../../src/crypto/api'; import { DeviceInfo } from '../../../src/crypto/deviceinfo'; @@ -41,7 +41,7 @@ async function makeTestClient(userInfo: { userId: string, deviceId: string}, opt await client.initCrypto(); // No need to download keys for these tests - jest.spyOn(client.crypto, 'downloadKeys').mockResolvedValue({}); + jest.spyOn(client.crypto!, 'downloadKeys').mockResolvedValue({}); return client; } @@ -93,11 +93,11 @@ describe("Secrets", function() { }, }, ); - alice.crypto.crossSigningInfo.setKeys({ + alice.crypto!.crossSigningInfo.setKeys({ master: signingkeyInfo, }); - const secretStorage = alice.crypto.secretStorage; + const secretStorage = alice.crypto!.secretStorage; jest.spyOn(alice, 'setAccountData').mockImplementation( async function(eventType, contents) { @@ -113,7 +113,7 @@ describe("Secrets", function() { const keyAccountData = { algorithm: SECRET_STORAGE_ALGORITHM_V1_AES, }; - await alice.crypto.crossSigningInfo.signObject(keyAccountData, 'master'); + await alice.crypto!.crossSigningInfo.signObject(keyAccountData, 'master'); alice.store.storeAccountDataEvents([ new MatrixEvent({ @@ -200,7 +200,7 @@ describe("Secrets", function() { await alice.storeSecret("foo", "bar"); const accountData = alice.getAccountData('foo'); - expect(accountData.getContent().encrypted).toBeTruthy(); + expect(accountData!.getContent().encrypted).toBeTruthy(); alice.stopClient(); }); @@ -233,29 +233,29 @@ describe("Secrets", function() { }, ); - const vaxDevice = vax.client.crypto.olmDevice; - const osborne2Device = osborne2.client.crypto.olmDevice; - const secretStorage = osborne2.client.crypto.secretStorage; + const vaxDevice = vax.client.crypto!.olmDevice; + const osborne2Device = osborne2.client.crypto!.olmDevice; + const secretStorage = osborne2.client.crypto!.secretStorage; - osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", { + osborne2.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", { "VAX": { known: false, algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], keys: { - "ed25519:VAX": vaxDevice.deviceEd25519Key, - "curve25519:VAX": vaxDevice.deviceCurve25519Key, + "ed25519:VAX": vaxDevice.deviceEd25519Key!, + "curve25519:VAX": vaxDevice.deviceCurve25519Key!, }, verified: DeviceInfo.DeviceVerification.VERIFIED, }, }); - vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", { + vax.client.crypto!.deviceList.storeDevicesForUser("@alice:example.com", { "Osborne2": { algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], verified: 0, known: false, keys: { - "ed25519:Osborne2": osborne2Device.deviceEd25519Key, - "curve25519:Osborne2": osborne2Device.deviceCurve25519Key, + "ed25519:Osborne2": osborne2Device.deviceEd25519Key!, + "curve25519:Osborne2": osborne2Device.deviceCurve25519Key!, }, }, }); @@ -264,13 +264,13 @@ describe("Secrets", function() { const otks = (await osborne2Device.getOneTimeKeys()).curve25519; await osborne2Device.markKeysAsPublished(); - await vax.client.crypto.olmDevice.createOutboundSession( - osborne2Device.deviceCurve25519Key, + await vax.client.crypto!.olmDevice.createOutboundSession( + osborne2Device.deviceCurve25519Key!, Object.values(otks)[0], ); - osborne2.client.crypto.deviceList.downloadKeys = () => Promise.resolve({}); - osborne2.client.crypto.deviceList.getUserByIdentityKey = () => "@alice:example.com"; + osborne2.client.crypto!.deviceList.downloadKeys = () => Promise.resolve({}); + osborne2.client.crypto!.deviceList.getUserByIdentityKey = () => "@alice:example.com"; const request = await secretStorage.request("foo", ["VAX"]); await request.promise; // return value not used @@ -328,7 +328,7 @@ describe("Secrets", function() { this.store.storeAccountDataEvents([ event, ]); - this.emit("accountData", event); + this.emit(ClientEvent.AccountData, event); return {}; }; @@ -339,8 +339,8 @@ describe("Secrets", function() { createSecretStorageKey, }); - const crossSigning = bob.crypto.crossSigningInfo; - const secretStorage = bob.crypto.secretStorage; + const crossSigning = bob.crypto!.crossSigningInfo; + const secretStorage = bob.crypto!.secretStorage; expect(crossSigning.getId()).toBeTruthy(); expect(await crossSigning.isStoredInSecretStorage(secretStorage)) @@ -486,7 +486,7 @@ describe("Secrets", function() { }, }), ]); - alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", { + alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", { firstUse: false, crossSigningVerifiedBefore: false, keys: { @@ -528,16 +528,15 @@ describe("Secrets", function() { content: data, }); alice.store.storeAccountDataEvents([event]); - this.emit("accountData", event); + this.emit(ClientEvent.AccountData, event); return {}; }; await alice.bootstrapSecretStorage({}); - expect(alice.getAccountData("m.secret_storage.default_key").getContent()) + expect(alice.getAccountData("m.secret_storage.default_key")!.getContent()) .toEqual({ key: "key_id" }); - const keyInfo = alice.getAccountData("m.secret_storage.key.key_id") - .getContent() as ISecretStorageKeyInfo; + const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")!.getContent(); expect(keyInfo.algorithm) .toEqual("m.secret_storage.v1.aes-hmac-sha2"); expect(keyInfo.passphrase).toEqual({ @@ -630,7 +629,7 @@ describe("Secrets", function() { }, }), ]); - alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", { + alice.crypto!.deviceList.storeCrossSigningForUser("@alice:example.com", { firstUse: false, crossSigningVerifiedBefore: false, keys: { @@ -672,14 +671,13 @@ describe("Secrets", function() { content: data, }); alice.store.storeAccountDataEvents([event]); - this.emit("accountData", event); + this.emit(ClientEvent.AccountData, event); return {}; }; await alice.bootstrapSecretStorage({}); - const backupKey = alice.getAccountData("m.megolm_backup.v1") - .getContent(); + const backupKey = alice.getAccountData("m.megolm_backup.v1")!.getContent(); expect(backupKey.encrypted).toHaveProperty("key_id"); expect(await alice.getSecret("m.megolm_backup.v1")) .toEqual("ey0GB1kB6jhOWgwiBUMIWg=="); diff --git a/spec/unit/crypto/verification/request.spec.ts b/spec/unit/crypto/verification/request.spec.ts index 10b29bf9fee..cd32ce264dd 100644 --- a/spec/unit/crypto/verification/request.spec.ts +++ b/spec/unit/crypto/verification/request.spec.ts @@ -49,7 +49,7 @@ describe("verification request integration tests with crypto layer", function() verificationMethods: [verificationMethods.SAS], }, ); - alice.client.crypto.deviceList.getRawStoredDevicesForUser = function() { + alice.client.crypto!.deviceList.getRawStoredDevicesForUser = function() { return { Dynabook: { algorithms: [], diff --git a/spec/unit/crypto/verification/sas.spec.ts b/spec/unit/crypto/verification/sas.spec.ts index 32932b7e5b8..559ecf351b5 100644 --- a/spec/unit/crypto/verification/sas.spec.ts +++ b/spec/unit/crypto/verification/sas.spec.ts @@ -17,16 +17,17 @@ limitations under the License. import "../../../olm-loader"; import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util'; import { MatrixEvent } from "../../../../src/models/event"; -import { SAS } from "../../../../src/crypto/verification/SAS"; +import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS"; import { DeviceInfo } from "../../../../src/crypto/deviceinfo"; import { CryptoEvent, verificationMethods } from "../../../../src/crypto"; import * as olmlib from "../../../../src/crypto/olmlib"; import { logger } from "../../../../src/logger"; import { resetCrossSigningKeys } from "../crypto-utils"; -import { VerificationBase } from "../../../../src/crypto/verification/Base"; +import { VerificationBase as Verification, VerificationBase } from "../../../../src/crypto/verification/Base"; import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel"; import { MatrixClient } from "../../../../src"; import { VerificationRequest } from "../../../../src/crypto/verification/request/VerificationRequest"; +import { TestClient } from "../../../TestClient"; const Olm = global.Olm; @@ -75,13 +76,13 @@ describe("SAS verification", function() { }); describe("verification", () => { - let alice; - let bob; - let aliceSasEvent; - let bobSasEvent; - let aliceVerifier; - let bobPromise; - let clearTestClientTimeouts; + let alice: TestClient; + let bob: TestClient; + let aliceSasEvent: ISasEvent | null; + let bobSasEvent: ISasEvent | null; + let aliceVerifier: Verification; + let bobPromise: Promise>; + let clearTestClientTimeouts: () => void; beforeEach(async () => { [[alice, bob], clearTestClientTimeouts] = await makeTestClients( @@ -94,8 +95,8 @@ describe("SAS verification", function() { }, ); - const aliceDevice = alice.client.crypto.olmDevice; - const bobDevice = bob.client.crypto.olmDevice; + const aliceDevice = alice.client.crypto!.olmDevice; + const bobDevice = bob.client.crypto!.olmDevice; ALICE_DEVICES = { Osborne2: { @@ -121,26 +122,26 @@ describe("SAS verification", function() { }, }; - alice.client.crypto.deviceList.storeDevicesForUser( + alice.client.crypto!.deviceList.storeDevicesForUser( "@bob:example.com", BOB_DEVICES, ); alice.client.downloadKeys = () => { - return Promise.resolve(); + return Promise.resolve({}); }; - bob.client.crypto.deviceList.storeDevicesForUser( + bob.client.crypto!.deviceList.storeDevicesForUser( "@alice:example.com", ALICE_DEVICES, ); bob.client.downloadKeys = () => { - return Promise.resolve(); + return Promise.resolve({}); }; aliceSasEvent = null; bobSasEvent = null; - bobPromise = new Promise((resolve, reject) => { - bob.client.on("crypto.verification.request", request => { - request.verifier.on("show_sas", (e) => { + bobPromise = new Promise>((resolve, reject) => { + bob.client.on(CryptoEvent.VerificationRequest, request => { + request.verifier!.on("show_sas", (e) => { if (!e.sas.emoji || !e.sas.decimal) { e.cancel(); } else if (!aliceSasEvent) { @@ -156,14 +157,14 @@ describe("SAS verification", function() { } } }); - resolve(request.verifier); + resolve(request.verifier!); }); }); aliceVerifier = alice.client.beginKeyVerification( - verificationMethods.SAS, bob.client.getUserId(), bob.deviceId, + verificationMethods.SAS, bob.client.getUserId()!, bob.deviceId!, ); - aliceVerifier.on("show_sas", (e) => { + aliceVerifier.on(SasEvent.ShowSas, (e) => { if (!e.sas.emoji || !e.sas.decimal) { e.cancel(); } else if (!bobSasEvent) { @@ -195,9 +196,9 @@ describe("SAS verification", function() { const origSendToDevice = bob.client.sendToDevice.bind(bob.client); bob.client.sendToDevice = function(type, map) { if (type === "m.key.verification.accept") { - macMethod = map[alice.client.getUserId()][alice.client.deviceId] + macMethod = map[alice.client.getUserId()!][alice.client.deviceId!] .message_authentication_code; - keyAgreement = map[alice.client.getUserId()][alice.client.deviceId] + keyAgreement = map[alice.client.getUserId()!][alice.client.deviceId!] .key_agreement_protocol; } return origSendToDevice(type, map); @@ -219,8 +220,8 @@ describe("SAS verification", function() { await Promise.all([ aliceVerifier.verify(), bobPromise.then((verifier) => verifier.verify()), - alice.httpBackend.flush(), - bob.httpBackend.flush(), + alice.httpBackend.flush(undefined), + bob.httpBackend.flush(undefined), ]); // make sure that it uses the preferred method @@ -230,10 +231,10 @@ describe("SAS verification", function() { // make sure Alice and Bob verified each other const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook"); - expect(bobDevice.isVerified()).toBeTruthy(); + expect(bobDevice?.isVerified()).toBeTruthy(); const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2"); - expect(aliceDevice.isVerified()).toBeTruthy(); + expect(aliceDevice?.isVerified()).toBeTruthy(); }); it("should be able to verify using the old base64", async () => { @@ -248,7 +249,7 @@ describe("SAS verification", function() { // has, since it is the same object. If this does not // happen, the verification will fail due to a hash // commitment mismatch. - map[bob.client.getUserId()][bob.client.deviceId] + map[bob.client.getUserId()!][bob.client.deviceId!] .message_authentication_codes = ['hkdf-hmac-sha256']; } return aliceOrigSendToDevice(type, map); @@ -256,7 +257,7 @@ describe("SAS verification", function() { const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client); bob.client.sendToDevice = (type, map) => { if (type === "m.key.verification.accept") { - macMethod = map[alice.client.getUserId()][alice.client.deviceId] + macMethod = map[alice.client.getUserId()!][alice.client.deviceId!] .message_authentication_code; } return bobOrigSendToDevice(type, map); @@ -278,18 +279,18 @@ describe("SAS verification", function() { await Promise.all([ aliceVerifier.verify(), bobPromise.then((verifier) => verifier.verify()), - alice.httpBackend.flush(), - bob.httpBackend.flush(), + alice.httpBackend.flush(undefined), + bob.httpBackend.flush(undefined), ]); expect(macMethod).toBe("hkdf-hmac-sha256"); const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook"); - expect(bobDevice.isVerified()).toBeTruthy(); + expect(bobDevice!.isVerified()).toBeTruthy(); const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2"); - expect(aliceDevice.isVerified()).toBeTruthy(); + expect(aliceDevice!.isVerified()).toBeTruthy(); }); it("should be able to verify using the old MAC", async () => { @@ -304,7 +305,7 @@ describe("SAS verification", function() { // has, since it is the same object. If this does not // happen, the verification will fail due to a hash // commitment mismatch. - map[bob.client.getUserId()][bob.client.deviceId] + map[bob.client.getUserId()!][bob.client.deviceId!] .message_authentication_codes = ['hmac-sha256']; } return aliceOrigSendToDevice(type, map); @@ -312,7 +313,7 @@ describe("SAS verification", function() { const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client); bob.client.sendToDevice = (type, map) => { if (type === "m.key.verification.accept") { - macMethod = map[alice.client.getUserId()][alice.client.deviceId] + macMethod = map[alice.client.getUserId()!][alice.client.deviceId!] .message_authentication_code; } return bobOrigSendToDevice(type, map); @@ -334,18 +335,18 @@ describe("SAS verification", function() { await Promise.all([ aliceVerifier.verify(), bobPromise.then((verifier) => verifier.verify()), - alice.httpBackend.flush(), - bob.httpBackend.flush(), + alice.httpBackend.flush(undefined), + bob.httpBackend.flush(undefined), ]); expect(macMethod).toBe("hmac-sha256"); const bobDevice = await alice.client.getStoredDevice("@bob:example.com", "Dynabook"); - expect(bobDevice.isVerified()).toBeTruthy(); + expect(bobDevice?.isVerified()).toBeTruthy(); const aliceDevice = await bob.client.getStoredDevice("@alice:example.com", "Osborne2"); - expect(aliceDevice.isVerified()).toBeTruthy(); + expect(aliceDevice?.isVerified()).toBeTruthy(); }); it("should verify a cross-signing key", async () => { @@ -361,9 +362,11 @@ describe("SAS verification", function() { await resetCrossSigningKeys(bob.client); - bob.client.crypto.deviceList.storeCrossSigningForUser( + bob.client.crypto!.deviceList.storeCrossSigningForUser( "@alice:example.com", { - keys: alice.client.crypto.crossSigningInfo.keys, + keys: alice.client.crypto!.crossSigningInfo.keys, + crossSigningVerifiedBefore: false, + firstUse: true, }, ); @@ -415,10 +418,10 @@ describe("SAS verification", function() { const bobPromise = new Promise>((resolve, reject) => { bob.client.on(CryptoEvent.VerificationRequest, request => { - request.verifier.on("show_sas", (e) => { + request.verifier!.on("show_sas", (e) => { e.mismatch(); }); - resolve(request.verifier); + resolve(request.verifier!); }); }); @@ -464,7 +467,7 @@ describe("SAS verification", function() { }, ); - alice.client.crypto.setDeviceVerification = jest.fn(); + alice.client.crypto!.setDeviceVerification = jest.fn(); alice.client.getDeviceEd25519Key = () => { return "alice+base64+ed25519+key"; }; @@ -482,7 +485,7 @@ describe("SAS verification", function() { return Promise.resolve(); }; - bob.client.crypto.setDeviceVerification = jest.fn(); + bob.client.crypto!.setDeviceVerification = jest.fn(); bob.client.getStoredDevice = () => { return DeviceInfo.fromStorage( { @@ -565,7 +568,7 @@ describe("SAS verification", function() { ]); // make sure Alice and Bob verified each other - expect(alice.client.crypto.setDeviceVerification) + expect(alice.client.crypto!.setDeviceVerification) .toHaveBeenCalledWith( bob.client.getUserId(), bob.client.deviceId, @@ -574,7 +577,7 @@ describe("SAS verification", function() { null, { "ed25519:Dynabook": "bob+base64+ed25519+key" }, ); - expect(bob.client.crypto.setDeviceVerification) + expect(bob.client.crypto!.setDeviceVerification) .toHaveBeenCalledWith( alice.client.getUserId(), alice.client.deviceId, diff --git a/spec/unit/crypto/verification/util.ts b/spec/unit/crypto/verification/util.ts index d7c519e74ae..7c19e892641 100644 --- a/spec/unit/crypto/verification/util.ts +++ b/spec/unit/crypto/verification/util.ts @@ -41,7 +41,7 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[] }); const client = clientMap[userId][deviceId]; const decryptionPromise = event.isEncrypted() ? - event.attemptDecryption(client.crypto) : + event.attemptDecryption(client.crypto!) : Promise.resolve(); decryptionPromise.then( diff --git a/spec/unit/event-mapper.spec.ts b/spec/unit/event-mapper.spec.ts index c21348c80e4..0dc43fc7a8b 100644 --- a/spec/unit/event-mapper.spec.ts +++ b/spec/unit/event-mapper.spec.ts @@ -32,7 +32,7 @@ describe("eventMapperFor", function() { fetchFn: function() {} as any, // NOP store: { getRoom(roomId: string): Room | null { - return rooms.find(r => r.roomId === roomId); + return rooms.find(r => r.roomId === roomId) ?? null; }, } as IStore, scheduler: { diff --git a/spec/unit/event-timeline-set.spec.ts b/spec/unit/event-timeline-set.spec.ts index e6c45fbd460..5e2d904eb20 100644 --- a/spec/unit/event-timeline-set.spec.ts +++ b/spec/unit/event-timeline-set.spec.ts @@ -50,8 +50,8 @@ describe('EventTimelineSet', () => { EventType.RoomMessage, ); expect(relations).toBeDefined(); - expect(relations.getRelations().length).toBe(1); - expect(relations.getRelations()[0].getId()).toBe(replyEvent.getId()); + expect(relations!.getRelations().length).toBe(1); + expect(relations!.getRelations()[0].getId()).toBe(replyEvent.getId()); }); }; diff --git a/spec/unit/event-timeline.spec.ts b/spec/unit/event-timeline.spec.ts index f7c346b7336..d96472887eb 100644 --- a/spec/unit/event-timeline.spec.ts +++ b/spec/unit/event-timeline.spec.ts @@ -21,7 +21,7 @@ describe("EventTimeline", function() { const getTimeline = (): EventTimeline => { const room = new Room(roomId, mockClient, userA); const timelineSet = new EventTimelineSet(room); - jest.spyOn(timelineSet.room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet); + jest.spyOn(room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet); return new EventTimeline(timelineSet); }; diff --git a/spec/unit/filter.spec.ts b/spec/unit/filter.spec.ts index dbeb932e679..ef8d67e5140 100644 --- a/spec/unit/filter.spec.ts +++ b/spec/unit/filter.spec.ts @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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 { UNREAD_THREAD_NOTIFICATIONS } from "../../src/@types/sync"; import { Filter, IFilterDefinition } from "../../src/filter"; import { mkEvent } from "../test-utils/test-utils"; diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index 458c05f203e..91687e8ad2b 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -36,7 +36,7 @@ import { ReceiptType } from "../../src/@types/read_receipts"; import * as testUtils from "../test-utils/test-utils"; import { makeBeaconInfoContent } from "../../src/content-helpers"; import { M_BEACON_INFO } from "../../src/@types/beacon"; -import { ContentHelpers, EventTimeline, Room } from "../../src"; +import { ContentHelpers, EventTimeline, MatrixError, Room } from "../../src"; import { supportsMatrixCall } from "../../src/webrtc/call"; import { makeBeaconEvent } from "../test-utils/beacon"; import { @@ -88,21 +88,22 @@ describe("MatrixClient", function() { data: SYNC_DATA, }; - let httpLookups = [ - // items are objects which look like: - // { - // method: "GET", - // path: "/initialSync", - // data: {}, - // error: { errcode: M_FORBIDDEN } // if present will reject promise, - // expectBody: {} // additional expects on the body - // expectQueryParams: {} // additional expects on query params - // thenCall: function(){} // function to call *AFTER* returning response. - // } - // items are popped off when processed and block if no items left. - ]; + // items are popped off when processed and block if no items left. + let httpLookups: { + method: string; + path: string; + data?: object; + error?: object; + expectBody?: object; + expectQueryParams?: object; + thenCall?: Function; + }[] = []; let acceptKeepalives: boolean; - let pendingLookup = null; + let pendingLookup: { + promise: Promise; + method: string; + path: string; + } | null = null; function httpReq(method, path, qp, data, prefix) { if (path === KEEP_ALIVE_PATH && acceptKeepalives) { return Promise.resolve({ @@ -144,7 +145,7 @@ describe("MatrixClient", function() { } if (next.expectQueryParams) { Object.keys(next.expectQueryParams).forEach(function(k) { - expect(qp[k]).toEqual(next.expectQueryParams[k]); + expect(qp[k]).toEqual(next.expectQueryParams![k]); }); } @@ -155,9 +156,9 @@ describe("MatrixClient", function() { if (next.error) { // eslint-disable-next-line return Promise.reject({ - errcode: next.error.errcode, - httpStatus: next.error.httpStatus, - name: next.error.errcode, + errcode: (next.error).errcode, + httpStatus: (next.error).httpStatus, + name: (next.error).errcode, message: "Expected testing error", data: next.error, }); @@ -254,7 +255,7 @@ describe("MatrixClient", function() { type: UNSTABLE_MSC3088_PURPOSE.unstable, state_key: UNSTABLE_MSC3089_TREE_SUBTYPE.unstable, content: { - [UNSTABLE_MSC3088_ENABLED.unstable]: true, + [UNSTABLE_MSC3088_ENABLED.unstable!]: true, }, }, { @@ -299,7 +300,7 @@ describe("MatrixClient", function() { expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable); return new MatrixEvent({ content: { - [UNSTABLE_MSC3088_ENABLED.unstable]: true, + [UNSTABLE_MSC3088_ENABLED.unstable!]: true, }, }); } else { @@ -359,7 +360,7 @@ describe("MatrixClient", function() { expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable); return new MatrixEvent({ content: { - [UNSTABLE_MSC3088_ENABLED.unstable]: true, + [UNSTABLE_MSC3088_ENABLED.unstable!]: true, }, }); } else { @@ -393,7 +394,7 @@ describe("MatrixClient", function() { expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable); return new MatrixEvent({ content: { - [UNSTABLE_MSC3088_ENABLED.unstable]: false, + [UNSTABLE_MSC3088_ENABLED.unstable!]: false, }, }); } else { @@ -599,14 +600,14 @@ describe("MatrixClient", function() { } it("should transition null -> PREPARED after the first /sync", function(done) { - const expectedStates = []; + const expectedStates: [string, string | null][] = []; expectedStates.push(["PREPARED", null]); client.on("sync", syncChecker(expectedStates, done)); client.startClient(); }); it("should transition null -> ERROR after a failed /filter", function(done) { - const expectedStates = []; + const expectedStates: [string, string | null][] = []; httpLookups = []; httpLookups.push(PUSH_RULES_RESPONSE); httpLookups.push({ @@ -620,36 +621,35 @@ describe("MatrixClient", function() { // Disabled because now `startClient` makes a legit call to `/versions` // And those tests are really unhappy about it... Not possible to figure // out what a good resolution would look like - xit("should transition ERROR -> CATCHUP after /sync if prev failed", - function(done) { - const expectedStates = []; - acceptKeepalives = false; - httpLookups = []; - httpLookups.push(PUSH_RULES_RESPONSE); - httpLookups.push(FILTER_RESPONSE); - httpLookups.push({ - method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }, - }); - httpLookups.push({ - method: "GET", path: KEEP_ALIVE_PATH, - error: { errcode: "KEEPALIVE_FAIL" }, - }); - httpLookups.push({ - method: "GET", path: KEEP_ALIVE_PATH, data: {}, - }); - httpLookups.push({ - method: "GET", path: "/sync", data: SYNC_DATA, - }); - - expectedStates.push(["RECONNECTING", null]); - expectedStates.push(["ERROR", "RECONNECTING"]); - expectedStates.push(["CATCHUP", "ERROR"]); - client.on("sync", syncChecker(expectedStates, done)); - client.startClient(); + xit("should transition ERROR -> CATCHUP after /sync if prev failed", function(done) { + const expectedStates: [string, string | null][] = []; + acceptKeepalives = false; + httpLookups = []; + httpLookups.push(PUSH_RULES_RESPONSE); + httpLookups.push(FILTER_RESPONSE); + httpLookups.push({ + method: "GET", path: "/sync", error: { errcode: "NOPE_NOPE_NOPE" }, + }); + httpLookups.push({ + method: "GET", path: KEEP_ALIVE_PATH, + error: { errcode: "KEEPALIVE_FAIL" }, + }); + httpLookups.push({ + method: "GET", path: KEEP_ALIVE_PATH, data: {}, + }); + httpLookups.push({ + method: "GET", path: "/sync", data: SYNC_DATA, }); + expectedStates.push(["RECONNECTING", null]); + expectedStates.push(["ERROR", "RECONNECTING"]); + expectedStates.push(["CATCHUP", "ERROR"]); + client.on("sync", syncChecker(expectedStates, done)); + client.startClient(); + }); + it("should transition PREPARED -> SYNCING after /sync", function(done) { - const expectedStates = []; + const expectedStates: [string, string | null][] = []; expectedStates.push(["PREPARED", null]); expectedStates.push(["SYNCING", "PREPARED"]); client.on("sync", syncChecker(expectedStates, done)); @@ -658,7 +658,7 @@ describe("MatrixClient", function() { xit("should transition SYNCING -> ERROR after a failed /sync", function(done) { acceptKeepalives = false; - const expectedStates = []; + const expectedStates: [string, string | null][] = []; httpLookups.push({ method: "GET", path: "/sync", error: { errcode: "NONONONONO" }, }); @@ -675,37 +675,35 @@ describe("MatrixClient", function() { client.startClient(); }); - xit("should transition ERROR -> SYNCING after /sync if prev failed", - function(done) { - const expectedStates = []; - httpLookups.push({ - method: "GET", path: "/sync", error: { errcode: "NONONONONO" }, - }); - httpLookups.push(SYNC_RESPONSE); - - expectedStates.push(["PREPARED", null]); - expectedStates.push(["SYNCING", "PREPARED"]); - expectedStates.push(["ERROR", "SYNCING"]); - client.on("sync", syncChecker(expectedStates, done)); - client.startClient(); + xit("should transition ERROR -> SYNCING after /sync if prev failed", function(done) { + const expectedStates: [string, string | null][] = []; + httpLookups.push({ + method: "GET", path: "/sync", error: { errcode: "NONONONONO" }, }); + httpLookups.push(SYNC_RESPONSE); - it("should transition SYNCING -> SYNCING on subsequent /sync successes", - function(done) { - const expectedStates = []; - httpLookups.push(SYNC_RESPONSE); - httpLookups.push(SYNC_RESPONSE); - - expectedStates.push(["PREPARED", null]); - expectedStates.push(["SYNCING", "PREPARED"]); - expectedStates.push(["SYNCING", "SYNCING"]); - client.on("sync", syncChecker(expectedStates, done)); - client.startClient(); - }); + expectedStates.push(["PREPARED", null]); + expectedStates.push(["SYNCING", "PREPARED"]); + expectedStates.push(["ERROR", "SYNCING"]); + client.on("sync", syncChecker(expectedStates, done)); + client.startClient(); + }); + + it("should transition SYNCING -> SYNCING on subsequent /sync successes", function(done) { + const expectedStates: [string, string | null][] = []; + httpLookups.push(SYNC_RESPONSE); + httpLookups.push(SYNC_RESPONSE); + + expectedStates.push(["PREPARED", null]); + expectedStates.push(["SYNCING", "PREPARED"]); + expectedStates.push(["SYNCING", "SYNCING"]); + client.on("sync", syncChecker(expectedStates, done)); + client.startClient(); + }); xit("should transition ERROR -> ERROR if keepalive keeps failing", function(done) { acceptKeepalives = false; - const expectedStates = []; + const expectedStates: [string, string | null][] = []; httpLookups.push({ method: "GET", path: "/sync", error: { errcode: "NONONONONO" }, }); diff --git a/spec/unit/pushprocessor.spec.ts b/spec/unit/pushprocessor.spec.ts index db4d2a41752..87dec0f112b 100644 --- a/spec/unit/pushprocessor.spec.ts +++ b/spec/unit/pushprocessor.spec.ts @@ -209,32 +209,32 @@ describe('NotificationService', function() { msgtype: "m.text", }, }); - matrixClient.pushRules = PushProcessor.rewriteDefaultRules(matrixClient.pushRules); + matrixClient.pushRules = PushProcessor.rewriteDefaultRules(matrixClient.pushRules!); pushProcessor = new PushProcessor(matrixClient); }); // User IDs it('should bing on a user ID.', function() { - testEvent.event.content.body = "Hello @ali:matrix.org, how are you?"; + testEvent.event.content!.body = "Hello @ali:matrix.org, how are you?"; const actions = pushProcessor.actionsForEvent(testEvent); expect(actions.tweaks.highlight).toEqual(true); }); it('should bing on a partial user ID with an @.', function() { - testEvent.event.content.body = "Hello @ali, how are you?"; + testEvent.event.content!.body = "Hello @ali, how are you?"; const actions = pushProcessor.actionsForEvent(testEvent); expect(actions.tweaks.highlight).toEqual(true); }); it('should bing on a partial user ID without @.', function() { - testEvent.event.content.body = "Hello ali, how are you?"; + testEvent.event.content!.body = "Hello ali, how are you?"; const actions = pushProcessor.actionsForEvent(testEvent); expect(actions.tweaks.highlight).toEqual(true); }); it('should bing on a case-insensitive user ID.', function() { - testEvent.event.content.body = "Hello @AlI:matrix.org, how are you?"; + testEvent.event.content!.body = "Hello @AlI:matrix.org, how are you?"; const actions = pushProcessor.actionsForEvent(testEvent); expect(actions.tweaks.highlight).toEqual(true); }); @@ -242,13 +242,13 @@ describe('NotificationService', function() { // Display names it('should bing on a display name.', function() { - testEvent.event.content.body = "Hello Alice M, how are you?"; + testEvent.event.content!.body = "Hello Alice M, how are you?"; const actions = pushProcessor.actionsForEvent(testEvent); expect(actions.tweaks.highlight).toEqual(true); }); it('should bing on a case-insensitive display name.', function() { - testEvent.event.content.body = "Hello ALICE M, how are you?"; + testEvent.event.content!.body = "Hello ALICE M, how are you?"; const actions = pushProcessor.actionsForEvent(testEvent); expect(actions.tweaks.highlight).toEqual(true); }); @@ -256,43 +256,43 @@ describe('NotificationService', function() { // Bing words it('should bing on a bing word.', function() { - testEvent.event.content.body = "I really like coffee"; + testEvent.event.content!.body = "I really like coffee"; const actions = pushProcessor.actionsForEvent(testEvent); expect(actions.tweaks.highlight).toEqual(true); }); it('should bing on case-insensitive bing words.', function() { - testEvent.event.content.body = "Coffee is great"; + testEvent.event.content!.body = "Coffee is great"; const actions = pushProcessor.actionsForEvent(testEvent); expect(actions.tweaks.highlight).toEqual(true); }); it('should bing on wildcard (.*) bing words.', function() { - testEvent.event.content.body = "It was foomahbar I think."; + testEvent.event.content!.body = "It was foomahbar I think."; const actions = pushProcessor.actionsForEvent(testEvent); expect(actions.tweaks.highlight).toEqual(true); }); it('should bing on character group ([abc]) bing words.', function() { - testEvent.event.content.body = "Ping!"; + testEvent.event.content!.body = "Ping!"; let actions = pushProcessor.actionsForEvent(testEvent); expect(actions.tweaks.highlight).toEqual(true); - testEvent.event.content.body = "Pong!"; + testEvent.event.content!.body = "Pong!"; actions = pushProcessor.actionsForEvent(testEvent); expect(actions.tweaks.highlight).toEqual(true); }); it('should bing on character range ([a-z]) bing words.', function() { - testEvent.event.content.body = "I ate 6 pies"; + testEvent.event.content!.body = "I ate 6 pies"; const actions = pushProcessor.actionsForEvent(testEvent); expect(actions.tweaks.highlight).toEqual(true); }); it('should bing on character negation ([!a]) bing words.', function() { - testEvent.event.content.body = "boke"; + testEvent.event.content!.body = "boke"; let actions = pushProcessor.actionsForEvent(testEvent); expect(actions.tweaks.highlight).toEqual(true); - testEvent.event.content.body = "bake"; + testEvent.event.content!.body = "bake"; actions = pushProcessor.actionsForEvent(testEvent); expect(actions.tweaks.highlight).toEqual(false); }); @@ -316,7 +316,7 @@ describe('NotificationService', function() { // invalid it('should gracefully handle bad input.', function() { - testEvent.event.content.body = { "foo": "bar" }; + testEvent.event.content!.body = { "foo": "bar" }; const actions = pushProcessor.actionsForEvent(testEvent); expect(actions.tweaks.highlight).toEqual(false); }); diff --git a/spec/unit/relations.spec.ts b/spec/unit/relations.spec.ts index 091d95ea914..9826264fb62 100644 --- a/spec/unit/relations.spec.ts +++ b/spec/unit/relations.spec.ts @@ -18,6 +18,7 @@ import { EventTimelineSet } from "../../src/models/event-timeline-set"; import { MatrixEvent, MatrixEventEvent } from "../../src/models/event"; import { Room } from "../../src/models/room"; import { Relations } from "../../src/models/relations"; +import { TestClient } from "../TestClient"; describe("Relations", function() { it("should deduplicate annotations", function() { @@ -43,7 +44,7 @@ describe("Relations", function() { // Add the event once and check results { relations.addEvent(eventA); - const annotationsByKey = relations.getSortedAnnotationsByKey(); + const annotationsByKey = relations.getSortedAnnotationsByKey()!; expect(annotationsByKey.length).toEqual(1); const [key, events] = annotationsByKey[0]; expect(key).toEqual("👍️"); @@ -53,7 +54,7 @@ describe("Relations", function() { // Add the event again and expect the same { relations.addEvent(eventA); - const annotationsByKey = relations.getSortedAnnotationsByKey(); + const annotationsByKey = relations.getSortedAnnotationsByKey()!; expect(annotationsByKey.length).toEqual(1); const [key, events] = annotationsByKey[0]; expect(key).toEqual("👍️"); @@ -66,7 +67,7 @@ describe("Relations", function() { // Add the event again and expect the same { relations.addEvent(eventB); - const annotationsByKey = relations.getSortedAnnotationsByKey(); + const annotationsByKey = relations.getSortedAnnotationsByKey()!; expect(annotationsByKey.length).toEqual(1); const [key, events] = annotationsByKey[0]; expect(key).toEqual("👍️"); @@ -179,4 +180,28 @@ describe("Relations", function() { expect(badlyEditedTopic.replacingEvent()).toBe(null); expect(badlyEditedTopic.getContent().topic).toBe("topic"); }); + + it("getSortedAnnotationsByKey should return null for non-annotation relations", async () => { + const userId = "@user:server"; + const room = new Room("room123", new TestClient(userId).client, userId); + const relations = new Relations("m.replace", "m.room.message", room); + + // Create an instance of an annotation + const eventData = { + "sender": "@bob:example.com", + "type": "m.room.message", + "event_id": "$cZ1biX33ENJqIm00ks0W_hgiO_6CHrsAc3ZQrnLeNTw", + "room_id": "!pzVjCQSoQPpXQeHpmK:example.com", + "content": { + "m.relates_to": { + "event_id": "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o", + "rel_type": "m.replace", + }, + }, + }; + const eventA = new MatrixEvent(eventData); + + relations.addEvent(eventA); + expect(relations.getSortedAnnotationsByKey()).toBeNull(); + }); }); diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index 93aafde5939..ae85ee0dfad 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -601,7 +601,7 @@ describe("Room", function() { }); const resetTimelineTests = function(timelineSupport) { - let events = null; + let events: MatrixEvent[]; beforeEach(function() { room = new Room(roomId, new TestClient(userA).client, userA, { timelineSupport: timelineSupport }); @@ -1732,7 +1732,7 @@ describe("Room", function() { client.members.mockReturnValue({ chunk: [memberEvent] }); await room.loadMembersIfNeeded(); - const memberA = room.getMember("@user_a:bar"); + const memberA = room.getMember("@user_a:bar")!; expect(memberA.name).toEqual("User A"); }); }); @@ -2455,28 +2455,28 @@ describe("Room", function() { room.addLiveEvents(events); - const thread = threadRoot.getThread(); + const thread = threadRoot.getThread()!; expect(thread.rootEvent).toBe(threadRoot); const rootRelations = thread.timelineSet.relations.getChildEventsForEvent( threadRoot.getId(), RelationType.Annotation, EventType.Reaction, - ).getSortedAnnotationsByKey(); + )!.getSortedAnnotationsByKey(); expect(rootRelations).toHaveLength(1); - expect(rootRelations[0][0]).toEqual(rootReaction.getRelation().key); - expect(rootRelations[0][1].size).toEqual(1); - expect(rootRelations[0][1].has(rootReaction)).toBeTruthy(); + expect(rootRelations![0][0]).toEqual(rootReaction.getRelation()!.key); + expect(rootRelations![0][1].size).toEqual(1); + expect(rootRelations![0][1].has(rootReaction)).toBeTruthy(); const responseRelations = thread.timelineSet.relations.getChildEventsForEvent( threadResponse.getId(), RelationType.Annotation, EventType.Reaction, - ).getSortedAnnotationsByKey(); + )!.getSortedAnnotationsByKey(); expect(responseRelations).toHaveLength(1); - expect(responseRelations[0][0]).toEqual(threadReaction.getRelation().key); - expect(responseRelations[0][1].size).toEqual(1); - expect(responseRelations[0][1].has(threadReaction)).toBeTruthy(); + expect(responseRelations![0][0]).toEqual(threadReaction.getRelation()!.key); + expect(responseRelations![0][1].size).toEqual(1); + expect(responseRelations![0][1].has(threadReaction)).toBeTruthy(); }); }); diff --git a/spec/unit/timeline-window.spec.ts b/spec/unit/timeline-window.spec.ts index b8c84c422f7..f6dec04e786 100644 --- a/spec/unit/timeline-window.spec.ts +++ b/spec/unit/timeline-window.spec.ts @@ -37,7 +37,7 @@ const mockClient = { function createTimeline(numEvents = 3, baseIndex = 1): EventTimeline { const room = new Room(ROOM_ID, mockClient, USER_ID); const timelineSet = new EventTimelineSet(room); - jest.spyOn(timelineSet.room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet); + jest.spyOn(room, 'getUnfilteredTimelineSet').mockReturnValue(timelineSet); const timeline = new EventTimeline(timelineSet); @@ -170,7 +170,7 @@ describe("TimelineWindow", function() { beforeEach(() => { jest.clearAllMocks(); mockClient.getEventTimeline.mockResolvedValue(undefined); - mockClient.paginateEventTimeline.mockReturnValue(undefined); + mockClient.paginateEventTimeline.mockResolvedValue(false); }); describe("load", function() { diff --git a/spec/unit/webrtc/call.spec.ts b/spec/unit/webrtc/call.spec.ts index 9828dfaa372..01b110b4fad 100644 --- a/spec/unit/webrtc/call.spec.ts +++ b/spec/unit/webrtc/call.spec.ts @@ -26,17 +26,19 @@ import { MockRTCPeerConnection, } from "../../test-utils/webrtc"; import { CallFeed } from "../../../src/webrtc/callFeed"; +import { EventType, MatrixClient } from "../../../src"; +import { MediaHandler } from "../../../src/webrtc/mediaHandler"; const startVoiceCall = async (client: TestClient, call: MatrixCall): Promise => { const callPromise = call.placeVoiceCall(); - await client.httpBackend.flush(""); + await client.httpBackend!.flush(""); await callPromise; call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" }); }; describe('Call', function() { - let client; + let client: TestClient; let call; let prevNavigator; let prevDocument; @@ -71,10 +73,10 @@ describe('Call', function() { client = new TestClient("@alice:foo", "somedevice", "token", undefined, {}); // We just stub out sendEvent: we're not interested in testing the client's // event sending code here - client.client.sendEvent = () => {}; - client.client.mediaHandler = new MockMediaHandler; - client.client.getMediaHandler = () => client.client.mediaHandler; - client.httpBackend.when("GET", "/voip/turnServer").respond(200, {}); + client.client.sendEvent = (() => {}) as unknown as MatrixClient["sendEvent"]; + client.client["mediaHandler"] = new MockMediaHandler as unknown as MediaHandler; + client.client.getMediaHandler = () => client.client["mediaHandler"]!; + client.httpBackend!.when("GET", "/voip/turnServer").respond(200, {}); call = new MatrixCall({ client: client.client, roomId: '!foo:bar', @@ -237,7 +239,7 @@ describe('Call', function() { expect(identChangedCallback).toHaveBeenCalled(); - const ident = call.getRemoteAssertedIdentity(); + const ident = call.getRemoteAssertedIdentity()!; expect(ident.id).toEqual("@steve:example.com"); expect(ident.displayName).toEqual("Steve Gibbons"); @@ -306,19 +308,19 @@ describe('Call', function() { }); it("should fallback to answering with no video", async () => { - await client.httpBackend.flush(); + await client.httpBackend!.flush(undefined); call.shouldAnswerWithMediaType = (wantedValue: boolean) => wantedValue; - client.client.mediaHandler.getUserMediaStream = jest.fn().mockRejectedValue("reject"); + client.client["mediaHandler"].getUserMediaStream = jest.fn().mockRejectedValue("reject"); await call.answer(true, true); - expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(1, true, true); - expect(client.client.mediaHandler.getUserMediaStream).toHaveBeenNthCalledWith(2, true, false); + expect(client.client["mediaHandler"].getUserMediaStream).toHaveBeenNthCalledWith(1, true, true); + expect(client.client["mediaHandler"].getUserMediaStream).toHaveBeenNthCalledWith(2, true, false); }); it("should handle mid-call device changes", async () => { - client.client.mediaHandler.getUserMediaStream = jest.fn().mockReturnValue( + client.client["mediaHandler"].getUserMediaStream = jest.fn().mockReturnValue( new MockMediaStream( "stream", [ new MockMediaStreamTrack("audio_track", "audio"), @@ -424,7 +426,7 @@ describe('Call', function() { it("should choose opponent member", async () => { const callPromise = call.placeVoiceCall(); - await client.httpBackend.flush(); + await client.httpBackend!.flush(undefined); await callPromise; const opponentMember = { @@ -480,7 +482,7 @@ describe('Call', function() { it("should correctly generate local SDPStreamMetadata", async () => { const callPromise = call.placeCallWithCallFeeds([new CallFeed({ - client, + client: client.client, // @ts-ignore Mock stream: new MockMediaStream("local_stream1", [new MockMediaStreamTrack("track_id", "audio")]), roomId: call.roomId, @@ -489,7 +491,7 @@ describe('Call', function() { audioMuted: false, videoMuted: false, })]); - await client.httpBackend.flush(); + await client.httpBackend!.flush(undefined); await callPromise; call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" }); @@ -521,7 +523,7 @@ describe('Call', function() { const callPromise = call.placeCallWithCallFeeds([ new CallFeed({ - client, + client: client.client, userId: client.getUserId(), // @ts-ignore Mock stream: localUsermediaStream, @@ -531,7 +533,7 @@ describe('Call', function() { videoMuted: false, }), new CallFeed({ - client, + client: client.client, userId: client.getUserId(), // @ts-ignore Mock stream: localScreensharingStream, @@ -541,7 +543,7 @@ describe('Call', function() { videoMuted: false, }), ]); - await client.httpBackend.flush(); + await client.httpBackend!.flush(undefined); await callPromise; call.getOpponentMember = jest.fn().mockReturnValue({ userId: "@bob:bar.uk" }); @@ -586,14 +588,14 @@ describe('Call', function() { getLocalAge: () => null, }); call.feeds.push(new CallFeed({ - client, + client: client.client, userId: "remote_user_id", // @ts-ignore Mock stream: new MockMediaStream("remote_stream_id", [new MockMediaStreamTrack("remote_tack_id")]), id: "remote_feed_id", purpose: SDPStreamMetadataPurpose.Usermedia, })); - await client.httpBackend.flush(); + await client.httpBackend!.flush(undefined); await callPromise; const callHangupCallback = jest.fn(); @@ -664,10 +666,10 @@ describe('Call', function() { }); it("should return false if window or document are undefined", () => { - global.window = undefined; + global.window = undefined!; expect(supportsMatrixCall()).toBe(false); global.window = prevWindow; - global.document = undefined; + global.document = undefined!; expect(supportsMatrixCall()).toBe(false); }); @@ -685,9 +687,9 @@ describe('Call', function() { it("should return false if RTCPeerConnection & RTCSessionDescription " + "& RTCIceCandidate & mediaDevices are unavailable", () => { - global.window.RTCPeerConnection = undefined; - global.window.RTCSessionDescription = undefined; - global.window.RTCIceCandidate = undefined; + global.window.RTCPeerConnection = undefined!; + global.window.RTCSessionDescription = undefined!; + global.window.RTCIceCandidate = undefined!; // @ts-ignore - writing to a read-only property as we are simulating faulty browsers global.navigator.mediaDevices = undefined; expect(supportsMatrixCall()).toBe(false); @@ -752,4 +754,17 @@ describe('Call', function() { expect(call.pushLocalFeed).toHaveBeenCalled(); }); }); + + describe("transferToCall", () => { + it("should send the required events", async () => { + const targetCall = new MatrixCall({ client: client.client }); + const sendEvent = jest.spyOn(client.client, "sendEvent"); + await call.transferToCall(targetCall); + + const newCallId = (sendEvent.mock.calls[0][2] as any)!.await_call; + expect(sendEvent).toHaveBeenCalledWith(call.roomId, EventType.CallReplaces, expect.objectContaining({ + create_call: newCallId, + })); + }); + }); }); diff --git a/spec/unit/webrtc/callEventHandler.spec.ts b/spec/unit/webrtc/callEventHandler.spec.ts index a8103f0d5f2..c13969c7ec2 100644 --- a/spec/unit/webrtc/callEventHandler.spec.ts +++ b/spec/unit/webrtc/callEventHandler.spec.ts @@ -58,7 +58,7 @@ describe("callEventHandler", () => { client.on(CallEventHandlerEvent.Incoming, incomingCallEmitted); client.getSyncState = jest.fn().mockReturnValue(SyncState.Syncing); - client.emit(ClientEvent.Sync, SyncState.Syncing); + client.emit(ClientEvent.Sync, SyncState.Syncing, null); expect(incomingCallEmitted).not.toHaveBeenCalled(); }); diff --git a/src/NamespacedValue.ts b/src/NamespacedValue.ts index 9b8b3d408a8..dc028bec4ca 100644 --- a/src/NamespacedValue.ts +++ b/src/NamespacedValue.ts @@ -23,7 +23,10 @@ import { Optional } from "matrix-events-sdk/lib/types"; export class NamespacedValue { // Stable is optional, but one of the two parameters is required, hence the weird-looking types. // Goal is to to have developers explicitly say there is no stable value (if applicable). - public constructor(public readonly stable: S | null | undefined, public readonly unstable?: U) { + public constructor(stable: S, unstable: U); + public constructor(stable: S, unstable?: U); + public constructor(stable: null | undefined, unstable: U); + public constructor(public readonly stable?: S | null, public readonly unstable?: U) { if (!this.unstable && !this.stable) { throw new Error("One of stable or unstable values must be supplied"); } @@ -33,10 +36,10 @@ export class NamespacedValue { if (this.stable) { return this.stable; } - return this.unstable; + return this.unstable!; } - public get altName(): U | S | null { + public get altName(): U | S | null | undefined { if (!this.stable) { return null; } @@ -57,7 +60,7 @@ export class NamespacedValue { // this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class // so we can instantiate `NamespacedValue` as a default type for that namespace. public findIn(obj: any): Optional { - let val: T; + let val: T | undefined = undefined; if (this.name) { val = obj?.[this.name]; } @@ -91,7 +94,7 @@ export class ServerControlledNamespacedValue if (this.stable && !this.preferUnstable) { return this.stable; } - return this.unstable; + return this.unstable!; } } @@ -109,10 +112,10 @@ export class UnstableValue extends Namespace } public get name(): U { - return this.unstable; + return this.unstable!; } public get altName(): S { - return this.stable; + return this.stable!; } } diff --git a/src/ToDeviceMessageQueue.ts b/src/ToDeviceMessageQueue.ts index 70902150536..815927679a4 100644 --- a/src/ToDeviceMessageQueue.ts +++ b/src/ToDeviceMessageQueue.ts @@ -15,7 +15,7 @@ limitations under the License. */ import { logger } from "./logger"; -import { MatrixClient } from "./matrix"; +import { MatrixError, MatrixClient } from "./matrix"; import { IndexedToDeviceBatch, ToDeviceBatch, ToDeviceBatchWithTxnId, ToDevicePayload } from "./models/ToDeviceMessage"; import { MatrixScheduler } from "./scheduler"; @@ -72,7 +72,7 @@ export class ToDeviceMessageQueue { logger.debug("Attempting to send queued to-device messages"); this.sending = true; - let headBatch: IndexedToDeviceBatch; + let headBatch: IndexedToDeviceBatch | null; try { while (this.running) { headBatch = await this.client.store.getOldestToDeviceBatch(); @@ -90,11 +90,11 @@ export class ToDeviceMessageQueue { ++this.retryAttempts; // eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line new-cap - const retryDelay = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(null, this.retryAttempts, e); + const retryDelay = MatrixScheduler.RETRY_BACKOFF_RATELIMIT(null, this.retryAttempts, e); if (retryDelay === -1) { // the scheduler function doesn't differentiate between fatal errors and just getting // bored and giving up for now - if (Math.floor(e.httpStatus / 100) === 4) { + if (Math.floor((e).httpStatus! / 100) === 4) { logger.error("Fatal error when sending to-device message - dropping to-device batch!", e); await this.client.store.removeToDeviceBatch(headBatch!.id); } else { diff --git a/src/autodiscovery.ts b/src/autodiscovery.ts index 8bf87e5177e..f2128c0d549 100644 --- a/src/autodiscovery.ts +++ b/src/autodiscovery.ts @@ -347,7 +347,7 @@ export class AutoDiscovery { * @returns {Promise} Resolves to the domain's client config. Can * be an empty object. */ - public static async getRawClientConfig(domain: string): Promise { + public static async getRawClientConfig(domain?: string): Promise { if (!domain || typeof(domain) !== "string" || domain.length === 0) { throw new Error("'domain' must be a string of non-zero length"); } diff --git a/src/client.ts b/src/client.ts index 0d3cc550f46..5b87cbd75e8 100644 --- a/src/client.ts +++ b/src/client.ts @@ -434,7 +434,7 @@ export interface IStartClientOpts { } export interface IStoredClientOpts extends IStartClientOpts { - crypto: Crypto; + crypto?: Crypto; canResetEntireTimeline: ResetTimelineCallback; } @@ -697,41 +697,31 @@ export interface IMyDevice { [UNSTABLE_MSC3852_LAST_SEEN_UA.unstable]?: string; } -export interface IDownloadKeyResult { - failures: { [serverName: string]: object }; - device_keys: { - [userId: string]: { - [deviceId: string]: IDeviceKeys & { - unsigned?: { - device_display_name: string; - }; - }; +export interface Keys { + keys: { [keyId: string]: string }; + usage: string[]; + user_id: string; +} + +export interface SigningKeys extends Keys { + signatures: ISignatures; +} + +export interface DeviceKeys { + [deviceId: string]: IDeviceKeys & { + unsigned?: { + device_display_name: string; }; }; +} + +export interface IDownloadKeyResult { + failures: { [serverName: string]: object }; + device_keys: { [userId: string]: DeviceKeys }; // the following three fields were added in 1.1 - master_keys?: { - [userId: string]: { - keys: { [keyId: string]: string }; - usage: string[]; - user_id: string; - }; - }; - self_signing_keys?: { - [userId: string]: { - keys: { [keyId: string]: string }; - signatures: ISignatures; - usage: string[]; - user_id: string; - }; - }; - user_signing_keys?: { - [userId: string]: { - keys: { [keyId: string]: string }; - signatures: ISignatures; - usage: string[]; - user_id: string; - }; - }; + master_keys?: { [userId: string]: Keys }; + self_signing_keys?: { [userId: string]: SigningKeys }; + user_signing_keys?: { [userId: string]: SigningKeys }; } export interface IClaimOTKsResult { @@ -886,7 +876,7 @@ export type EmittedEvents = ClientEvent | BeaconEvent; export type ClientEventHandlerMap = { - [ClientEvent.Sync]: (state: SyncState, lastState?: SyncState, data?: ISyncStateData) => void; + [ClientEvent.Sync]: (state: SyncState, lastState: SyncState | null, data?: ISyncStateData) => void; [ClientEvent.Event]: (event: MatrixEvent) => void; [ClientEvent.ToDeviceEvent]: (event: MatrixEvent) => void; [ClientEvent.AccountData]: (event: MatrixEvent, lastEvent?: MatrixEvent) => void; @@ -943,22 +933,22 @@ export class MatrixClient extends TypedEventEmitter, errorTs?: number}} = {}; - protected notifTimelineSet: EventTimelineSet = null; + protected notifTimelineSet: EventTimelineSet | null = null; protected cryptoStore: CryptoStore; protected verificationMethods: VerificationMethod[]; protected fallbackICEServerAllowed = false; protected roomList: RoomList; - protected syncApi: SlidingSyncSdk | SyncApi; + protected syncApi?: SlidingSyncSdk | SyncApi; public roomNameGenerator?: ICreateClientOpts["roomNameGenerator"]; - public pushRules: IPushRules; - protected syncLeftRoomsPromise: Promise; + public pushRules?: IPushRules; + protected syncLeftRoomsPromise?: Promise; protected syncedLeftRooms = false; - protected clientOpts: IStoredClientOpts; - protected clientWellKnownIntervalID: ReturnType; - protected canResetTimelineCallback: ResetTimelineCallback; + protected clientOpts?: IStoredClientOpts; + protected clientWellKnownIntervalID?: ReturnType; + protected canResetTimelineCallback?: ResetTimelineCallback; public canSupport = new Map(); @@ -967,7 +957,7 @@ export class MatrixClient extends TypedEventEmitter; + protected serverVersionsPromise?: Promise; public cachedCapabilities: { capabilities: ICapabilities; @@ -1242,7 +1232,7 @@ export class MatrixClient extends TypedEventEmitter { + public async rehydrateDevice(): Promise { if (this.crypto) { throw new Error("Cannot rehydrate device after crypto is initialized"); } @@ -1317,7 +1307,7 @@ export class MatrixClient extends TypedEventEmitter { + ): Promise { if (!this.crypto) { logger.warn('not dehydrating device if crypto is not enabled'); return; @@ -1404,7 +1394,7 @@ export class MatrixClient extends TypedEventEmitter { + public async exportDevice(): Promise { if (!this.crypto) { logger.warn('not exporting device if crypto is not enabled'); return; @@ -1452,7 +1442,7 @@ export class MatrixClient extends TypedEventEmitter { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); @@ -2058,7 +2043,7 @@ export class MatrixClient extends TypedEventEmitter { + public getSecret(name: string): Promise { if (!this.crypto) { throw new Error("End-to-end encryption disabled"); } @@ -2656,7 +2641,10 @@ export class MatrixClient extends TypedEventEmitter { + public checkKeyBackup(): Promise { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } return this.crypto.backupManager.checkKeyBackup(); } @@ -2672,7 +2660,7 @@ export class MatrixClient extends TypedEventEmittere).errcode === 'M_NOT_FOUND') { return null; } else { throw e; @@ -2693,6 +2681,9 @@ export class MatrixClient extends TypedEventEmitter { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } return this.crypto.backupManager.isKeyBackupTrusted(info); } @@ -2701,7 +2692,7 @@ export class MatrixClient extends TypedEventEmitter> { if (!this.crypto) { @@ -3053,17 +3044,20 @@ export class MatrixClient extends TypedEventEmitter { + if (!this.crypto) { + throw new Error("End-to-end encryption disabled"); + } const storedKey = await this.getSecret("m.megolm_backup.v1"); // ensure that the key is in the right format. If not, fix the key and // store the fixed version const fixedKey = fixBackupKey(storedKey); if (fixedKey) { - const [keyId] = await this.crypto.getSecretStorageKey(); - await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]); + const keys = await this.crypto.getSecretStorageKey(); + await this.storeSecret("m.megolm_backup.v1", fixedKey, [keys![0]]); } - const privKey = decodeBase64(fixedKey || storedKey); + const privKey = decodeBase64(fixedKey || storedKey!); return this.restoreKeyBackup( privKey, targetRoomId, targetSessionId, backupInfo, opts, ); @@ -3406,7 +3400,7 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/user/$userId/account_data/$type", { - $userId: this.credentials.userId, + $userId: this.credentials.userId!, $type: eventType, }); return retryNetworkOperation(5, () => { @@ -3442,7 +3436,7 @@ export class MatrixClient extends TypedEventEmitter(); } const path = utils.encodeUri("/user/$userId/account_data/$type", { - $userId: this.credentials.userId, + $userId: this.credentials.userId!, $type: eventType, }); try { @@ -3506,7 +3500,7 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags", { - $userId: this.credentials.userId, + $userId: this.credentials.userId!, $roomId: roomId, }); return this.http.authedRequest(Method.Get, path); @@ -3636,7 +3630,7 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { - $userId: this.credentials.userId, + $userId: this.credentials.userId!, $roomId: roomId, $tag: tagName, }); @@ -3651,7 +3645,7 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/user/$userId/rooms/$roomId/tags/$tag", { - $userId: this.credentials.userId, + $userId: this.credentials.userId!, $roomId: roomId, $tag: tagName, }); @@ -3671,7 +3665,7 @@ export class MatrixClient extends TypedEventEmitter, ): Promise<{}> { const path = utils.encodeUri("/user/$userId/rooms/$roomId/account_data/$type", { - $userId: this.credentials.userId, + $userId: this.credentials.userId!, $roomId: roomId, $type: eventType, }); @@ -3734,8 +3728,7 @@ export class MatrixClient extends TypedEventEmitter e.getId() === targetId); - target.once(MatrixEventEvent.LocalEventIdReplaced, () => { + const target = room?.getPendingEvents().find(e => e.getId() === targetId); + target?.once(MatrixEventEvent.LocalEventIdReplaced, () => { localEvent.updateAssociatedId(target.getId()); }); } @@ -3879,13 +3872,13 @@ export class MatrixClient extends TypedEventEmitter { + private encryptAndSendEvent(room: Room | null, event: MatrixEvent): Promise { let cancelled = false; // Add an extra Promise.resolve() to turn synchronous exceptions into promise rejections, // so that we can handle synchronous and asynchronous exceptions with the // same code path. return Promise.resolve().then(() => { - const encryptionPromise = this.encryptEventIfNeeded(event, room); + const encryptionPromise = this.encryptEventIfNeeded(event, room ?? undefined); if (!encryptionPromise) return null; // doesn't need encryption this.pendingEventEncryption.set(event.getId(), encryptionPromise); @@ -3900,14 +3893,14 @@ export class MatrixClient extends TypedEventEmitter { if (cancelled) return {} as ISendEventResponse; - let promise: Promise; + let promise: Promise | null = null; if (this.scheduler) { // if this returns a promise then the scheduler has control now and will // resolve/reject when it is done. Internally, the scheduler will invoke // processFn which is set to this._sendEventHttpRequest so the same code // path is executed regardless. promise = this.scheduler.queueEvent(event); - if (promise && this.scheduler.getQueueForEvent(event).length > 1) { + if (promise && this.scheduler.getQueueForEvent(event)!.length > 1) { // event is processed FIFO so if the length is 2 or more we know // this event is stuck behind an earlier event. this.updatePendingEventStatus(room, event, EventStatus.QUEUED); @@ -3933,11 +3926,11 @@ export class MatrixClient extends TypedEventEmittere).stack || err); + } + if (err instanceof MatrixError) { + err.event = event; } throw err; }); @@ -4128,8 +4121,8 @@ export class MatrixClient extends TypedEventEmitter => { - let newEvent: IPartialEvent = null; + const makeContentExtensible = (content: IContent = {}, recurse = true): IPartialEvent | undefined => { + let newEvent: IPartialEvent | undefined; if (content['msgtype'] === MsgType.Text) { newEvent = MessageEvent.from(content['body'], content['formatted_body']).serialize(); @@ -4818,7 +4811,7 @@ export class MatrixClient extends TypedEventEmitter[] = []; const doLeave = (roomId: string) => { return this.leave(roomId).then(() => { @@ -4907,7 +4900,7 @@ export class MatrixClient extends TypedEventEmitter { // API returns an empty object @@ -5020,13 +5013,9 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri("/presence/$userId/status", { - $userId: this.credentials.userId, + $userId: this.credentials.userId!, }); - if (typeof opts === "string") { - opts = { presence: opts }; // legacy - } - const validStates = ["offline", "online", "unavailable"]; if (validStates.indexOf(opts.presence) === -1) { throw new Error("Bad presence value: " + opts.presence); @@ -5086,7 +5075,7 @@ export class MatrixClient extends TypedEventEmitter((resolve, reject) => { + const promise = new Promise((resolve, reject) => { // wait for a time before doing this request // (which may be 0 in order not to special case the code paths) sleep(timeToWaitMs).then(() => { @@ -5114,7 +5103,7 @@ export class MatrixClient extends TypedEventEmitter { this.ongoingScrollbacks[room.roomId] = { @@ -5124,13 +5113,10 @@ export class MatrixClient extends TypedEventEmitter | undefined = undefined; - if (this.clientOpts.lazyLoadMembers) { + if (this.clientOpts?.lazyLoadMembers) { params = { filter: JSON.stringify(Filter.LAZY_LOADING_MESSAGES_FILTER) }; } @@ -5343,7 +5329,7 @@ export class MatrixClient extends TypedEventEmitter { const isNotifTimeline = (eventTimeline.getTimelineSet() === this.notifTimelineSet); - const room = this.getRoom(eventTimeline.getRoomId()); + const room = this.getRoom(eventTimeline.getRoomId()!); const isThreadTimeline = eventTimeline.getTimelineSet().isThreadTimeline; // TODO: we should implement a backoff (as per scrollback()) to deal more @@ -5519,7 +5505,7 @@ export class MatrixClient extends TypedEventEmitter { - if (this.peekSync) { - this.peekSync.stopPeeking(); - } + this.peekSync?.stopPeeking(); this.peekSync = new SyncApi(this, this.clientOpts); return this.peekSync.peek(roomId); } @@ -5943,8 +5927,8 @@ export class MatrixClient extends TypedEventEmitter | void { - let promise: Promise; + public setRoomMutePushRule(scope: string, roomId: string, mute: boolean): Promise | undefined { + let promise: Promise | undefined; let hasDontNotifyRule = false; // Get the existing room-kind push rule if any @@ -5986,7 +5970,7 @@ export class MatrixClient extends TypedEventEmitter((resolve, reject) => { // Update this.pushRules when the operation completes - promise.then(() => { + promise!.then(() => { this.getPushRules().then((result) => { this.pushRules = result; resolve(); @@ -6173,7 +6157,7 @@ export class MatrixClient extends TypedEventEmitter { - this.syncLeftRoomsPromise = null; // cleanup ongoing request state + this.syncLeftRoomsPromise = undefined; // cleanup ongoing request state }); return this.syncLeftRoomsPromise; @@ -6235,13 +6219,12 @@ export class MatrixClient extends TypedEventEmitter { const filterId = this.store.getFilterIdByName(filterName); - let existingId = undefined; + let existingId: string | undefined; if (filterId) { // check that the existing filter matches our expectations try { - const existingFilter = - await this.getFilter(this.credentials.userId, filterId, true); + const existingFilter = await this.getFilter(this.credentials.userId!, filterId, true); if (existingFilter) { const oldDef = existingFilter.getDefinition(); const newDef = filter.getDefinition(); @@ -6260,7 +6243,7 @@ export class MatrixClient extends TypedEventEmittererror).errcode !== "M_UNKNOWN" && (error).errcode !== "M_NOT_FOUND") { throw error; } } @@ -6413,7 +6396,7 @@ export class MatrixClient extends TypedEventEmitter { const path = utils.encodeUri( "/_synapse/admin/v1/users/$userId/admin", - { $userId: this.getUserId() }, + { $userId: this.getUserId()! }, ); return this.http.authedRequest( Method.Get, path, undefined, undefined, { prefix: '' }, @@ -6452,7 +6435,7 @@ export class MatrixClient extends TypedEventEmitter { // `getRawClientConfig` does not throw or reject on network errors, instead // it absorbs errors and returns `{}`. - this.clientWellKnownPromise = AutoDiscovery.getRawClientConfig(this.getDomain()); + this.clientWellKnownPromise = AutoDiscovery.getRawClientConfig(this.getDomain() ?? undefined); this.clientWellKnown = await this.clientWellKnownPromise; this.emit(ClientEvent.ClientWellKnown, this.clientWellKnown); } @@ -6474,7 +6457,7 @@ export class MatrixClient extends TypedEventEmitter { // XXX: Intended private, used in code const primTypes = ["boolean", "string", "number"]; - const serializableOpts = Object.entries(this.clientOpts) + const serializableOpts = Object.entries(this.clientOpts!) .filter(([key, value]) => { return primTypes.includes(typeof value); }) @@ -6530,7 +6513,7 @@ export class MatrixClient extends TypedEventEmitter { // Need to unset this if it fails, otherwise we'll never retry - this.serverVersionsPromise = null; + this.serverVersionsPromise = undefined; // but rethrow the exception to anything that was waiting throw e; }); @@ -6692,7 +6675,7 @@ export class MatrixClient extends TypedEventEmitter { - return this.http.authedRequest(Method.Get, "/pushrules/").then((rules: IPushRules) => { + return this.http.authedRequest(Method.Get, "/pushrules/").then((rules: IPushRules) => { return PushProcessor.rewriteDefaultRules(rules); }); } @@ -8455,8 +8438,11 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types return this.http.idServerRequest( - Method.Get, "/hash_details", - null, IdentityPrefix.V2, identityAccessToken, + Method.Get, + "/hash_details", + undefined, + IdentityPrefix.V2, + identityAccessToken, ); } @@ -8530,7 +8516,7 @@ export class MatrixClient extends TypedEventEmitter { // TODO: Types // Note: we're using the V2 API by calling this function, but our // function contract requires a V1 response. We therefore have to // convert it manually. - const response = await this.identityHashedLookup( - [[address, medium]], identityAccessToken, - ); + const response = await this.identityHashedLookup([[address, medium]], identityAccessToken); const result = response.find(p => p.address === address); if (!result) { return {}; @@ -8608,7 +8592,7 @@ export class MatrixClient extends TypedEventEmitter [p[1], p[0]]), identityAccessToken, ); - const v1results = []; + const v1results: [medium: string, address: string, mxid: string][] = []; for (const mapping of response) { const originalQuery = query.find(p => p[1] === mapping.address); if (!originalQuery) { @@ -8800,7 +8784,7 @@ export class MatrixClient extends TypedEventEmitter = { + const queryParams: QueryDict = { suggested_only: String(suggestedOnly), max_depth: maxDepth?.toString(), from: fromToken, diff --git a/src/crypto/CrossSigning.ts b/src/crypto/CrossSigning.ts index 390720988a3..358e733e608 100644 --- a/src/crypto/CrossSigning.ts +++ b/src/crypto/CrossSigning.ts @@ -114,10 +114,10 @@ export class CrossSigningInfo { } if (expectedPubkey === undefined) { - expectedPubkey = this.getId(type); + expectedPubkey = this.getId(type)!; } - function validateKey(key: Uint8Array): [string, PkSigning] { + function validateKey(key: Uint8Array | null): [string, PkSigning] | undefined { if (!key) return; const signing = new global.Olm.PkSigning(); const gotPubkey = signing.init_with_seed(key); @@ -127,7 +127,7 @@ export class CrossSigningInfo { signing.free(); } - let privkey; + let privkey: Uint8Array | null = null; if (this.cacheCallbacks.getCrossSigningKeyCache && shouldCache) { privkey = await this.cacheCallbacks.getCrossSigningKeyCache(type, expectedPubkey); } @@ -141,7 +141,7 @@ export class CrossSigningInfo { const result = validateKey(privkey); if (result) { if (this.cacheCallbacks.storeCrossSigningKeyCache && shouldCache) { - await this.cacheCallbacks.storeCrossSigningKeyCache(type, privkey); + await this.cacheCallbacks.storeCrossSigningKeyCache(type, privkey!); } return result; } @@ -169,7 +169,7 @@ export class CrossSigningInfo { * with, or null if it is not present or not encrypted with a trusted * key */ - public async isStoredInSecretStorage(secretStorage: SecretStorage): Promise> { + public async isStoredInSecretStorage(secretStorage: SecretStorage): Promise | null> { // check what SSSS keys have encrypted the master key (if any) const stored = await secretStorage.isStored("m.cross_signing.master") || {}; // then check which of those SSSS keys have also encrypted the SSK and USK @@ -213,7 +213,7 @@ export class CrossSigningInfo { * @param {SecretStorage} secretStorage The secret store using account data * @return {Uint8Array} The private key */ - public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise { + public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise { const encodedKey = await secretStorage.get(`m.cross_signing.${type}`); if (!encodedKey) { return null; @@ -233,7 +233,7 @@ export class CrossSigningInfo { if (!cacheCallbacks) return false; const types = type ? [type] : ["master", "self_signing", "user_signing"]; for (const t of types) { - if (!await cacheCallbacks.getCrossSigningKeyCache(t)) { + if (!await cacheCallbacks.getCrossSigningKeyCache?.(t)) { return false; } } @@ -250,7 +250,7 @@ export class CrossSigningInfo { const cacheCallbacks = this.cacheCallbacks; if (!cacheCallbacks) return keys; for (const type of ["master", "self_signing", "user_signing"]) { - const privKey = await cacheCallbacks.getCrossSigningKeyCache(type); + const privKey = await cacheCallbacks.getCrossSigningKeyCache?.(type); if (!privKey) { continue; } @@ -268,7 +268,7 @@ export class CrossSigningInfo { * * @return {string} the ID */ - public getId(type = "master"): string { + public getId(type = "master"): string | null { if (!this.keys[type]) return null; const keyInfo = this.keys[type]; return publicKeyFromKeyInfo(keyInfo); @@ -469,7 +469,7 @@ export class CrossSigningInfo { } } - public async signUser(key: CrossSigningInfo): Promise { + public async signUser(key: CrossSigningInfo): Promise { if (!this.keys.user_signing) { logger.info("No user signing key: not signing user"); return; @@ -477,7 +477,7 @@ export class CrossSigningInfo { return this.signObject(key.keys.master, "user_signing"); } - public async signDevice(userId: string, device: DeviceInfo): Promise { + public async signDevice(userId: string, device: DeviceInfo): Promise { if (userId !== this.userId) { throw new Error( `Trying to sign ${userId}'s device; can only sign our own device`, @@ -521,9 +521,9 @@ export class CrossSigningInfo { return new UserTrustLevel(false, false, userCrossSigning.firstUse); } - let userTrusted; + let userTrusted: boolean; const userMaster = userCrossSigning.keys.master; - const uskId = this.getId('user_signing'); + const uskId = this.getId('user_signing')!; try { pkVerify(userMaster, uskId, this.userId); userTrusted = true; @@ -567,7 +567,7 @@ export class CrossSigningInfo { const deviceObj = deviceToObject(device, userCrossSigning.userId); try { // if we can verify the user's SSK from their master key... - pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId); + pkVerify(userSSK, userCrossSigning.getId()!, userCrossSigning.userId); // ...and this device's key from their SSK... pkVerify(deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId); // ...then we trust this device as much as far as we trust the user @@ -752,7 +752,7 @@ export type KeysDuringVerification = [[string, PkSigning], [string, PkSigning], * @param {string} userId The user ID being verified * @param {string} deviceId The device ID being verified */ -export function requestKeysDuringVerification( +export async function requestKeysDuringVerification( baseApis: MatrixClient, userId: string, deviceId: string, @@ -766,7 +766,7 @@ export function requestKeysDuringVerification( // it. We return here in order to test. return new Promise((resolve, reject) => { const client = baseApis; - const original = client.crypto.crossSigningInfo; + const original = client.crypto!.crossSigningInfo; // We already have all of the infrastructure we need to validate and // cache cross-signing keys, so instead of replicating that, here we set @@ -801,7 +801,7 @@ export function requestKeysDuringVerification( // also request and cache the key backup key const backupKeyPromise = (async () => { - const cachedKey = await client.crypto.getSessionBackupPrivateKey(); + const cachedKey = await client.crypto!.getSessionBackupPrivateKey(); if (!cachedKey) { logger.info("No cached backup key found. Requesting..."); const secretReq = client.requestSecret( @@ -811,13 +811,13 @@ export function requestKeysDuringVerification( logger.info("Got key backup key, decoding..."); const decodedKey = decodeBase64(base64Key); logger.info("Decoded backup key, storing..."); - await client.crypto.storeSessionBackupPrivateKey( + await client.crypto!.storeSessionBackupPrivateKey( Uint8Array.from(decodedKey), ); logger.info("Backup key stored. Starting backup restore..."); const backupInfo = await client.getKeyBackupVersion(); // no need to await for this - just let it go in the bg - client.restoreKeyBackupWithCache(undefined, undefined, backupInfo).then(() => { + client.restoreKeyBackupWithCache(undefined, undefined, backupInfo!).then(() => { logger.info("Backup restored."); }); } diff --git a/src/crypto/DeviceList.ts b/src/crypto/DeviceList.ts index c203ce5dae6..482f611dd53 100644 --- a/src/crypto/DeviceList.ts +++ b/src/crypto/DeviceList.ts @@ -26,7 +26,7 @@ import { CrossSigningInfo, ICrossSigningInfo } from './CrossSigning'; import * as olmlib from './olmlib'; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import { chunkPromises, defer, IDeferred, sleep } from '../utils'; -import { IDownloadKeyResult, MatrixClient } from "../client"; +import { DeviceKeys, IDownloadKeyResult, Keys, MatrixClient, SigningKeys } from "../client"; import { OlmDevice } from "./OlmDevice"; import { CryptoStore } from "./store/base"; import { TypedEventEmitter } from "../models/typed-event-emitter"; @@ -81,24 +81,24 @@ export class DeviceList extends TypedEventEmitter } = {}; + private keyDownloadsInProgressByUser = new Map>; // Set whenever changes are made other than setting the sync token private dirty = false; // Promise resolved when device data is saved - private savePromise: Promise = null; + private savePromise: Promise | null = null; // Function that resolves the save promise - private resolveSavePromise: (saved: boolean) => void = null; + private resolveSavePromise: ((saved: boolean) => void) | null = null; // The time the save is scheduled for - private savePromiseTime: number = null; + private savePromiseTime: number | null = null; // The timer used to delay the save - private saveTimer: ReturnType = null; + private saveTimer: ReturnType | null = null; // True if we have fetched data from the server or loaded a non-empty // set of device data from the store - private hasFetched: boolean = null; + private hasFetched: boolean | null = null; private readonly serialiser: DeviceListUpdateSerialiser; @@ -127,7 +127,7 @@ export class DeviceList extends TypedEventEmitter { // The device list is considered dirty until the write completes. this.dirty = false; - resolveSavePromise(true); + resolveSavePromise?.(true); }, err => { logger.error('Failed to save device tracking data', this.syncToken); logger.error(err); @@ -230,7 +230,7 @@ export class DeviceList extends TypedEventEmitter { const trackingStatus = this.deviceTrackingStatus[u]; - if (this.keyDownloadsInProgressByUser[u]) { + if (this.keyDownloadsInProgressByUser.has(u)) { // already a key download in progress/queued for this user; its results // will be good enough for us. logger.log( `downloadKeys: already have a download in progress for ` + `${u}: awaiting its result`, ); - promises.push(this.keyDownloadsInProgressByUser[u]); + promises.push(this.keyDownloadsInProgressByUser.get(u)!); } else if (forceDownload || trackingStatus != TrackingStatus.UpToDate) { usersToDownload.push(u); } @@ -341,7 +341,7 @@ export class DeviceList extends TypedEventEmitter { this.saveIfDirty(); - const usersToDownload = []; + const usersToDownload: string[] = []; for (const userId of Object.keys(this.deviceTrackingStatus)) { const stat = this.deviceTrackingStatus[userId]; if (stat == TrackingStatus.PendingDownload) { @@ -617,7 +614,7 @@ export class DeviceList extends TypedEventEmitter { + const prom = this.serialiser.updateDevicesForUsers(users, this.syncToken!).then(() => { finished(true); }, (e) => { logger.error( @@ -628,7 +625,7 @@ export class DeviceList extends TypedEventEmitter { - this.keyDownloadsInProgressByUser[u] = prom; + this.keyDownloadsInProgressByUser.set(u, prom); const stat = this.deviceTrackingStatus[u]; if (stat == TrackingStatus.PendingDownload) { this.deviceTrackingStatus[u] = TrackingStatus.DownloadInProgress; @@ -643,11 +640,11 @@ export class DeviceList extends TypedEventEmitter = null; + private queuedQueryDeferred?: IDeferred; - private syncToken: string = null; // The sync token we send with the requests + private syncToken?: string; // The sync token we send with the requests /* * @param {object} baseApis Base API object @@ -748,7 +745,7 @@ class DeviceListUpdateSerialiser { const downloadUsers = Object.keys(this.keyDownloadsQueuedByUser); this.keyDownloadsQueuedByUser = {}; const deferred = this.queuedQueryDeferred; - this.queuedQueryDeferred = null; + this.queuedQueryDeferred = undefined; logger.log('Starting key download for', downloadUsers); this.downloadInProgress = true; @@ -785,9 +782,9 @@ class DeviceListUpdateSerialiser { try { await this.processQueryResponseForUser( userId, dk[userId], { - master: masterKeys[userId], - self_signing: ssks[userId], - user_signing: usks[userId], + master: masterKeys?.[userId], + self_signing: ssks?.[userId], + user_signing: usks?.[userId], }, ); } catch (e) { @@ -800,7 +797,7 @@ class DeviceListUpdateSerialiser { logger.log('Completed key download for ' + downloadUsers); this.downloadInProgress = false; - deferred.resolve(); + deferred?.resolve(); // if we have queued users, fire off another request. if (this.queuedQueryDeferred) { @@ -809,19 +806,19 @@ class DeviceListUpdateSerialiser { }, (e) => { logger.warn('Error downloading keys for ' + downloadUsers + ':', e); this.downloadInProgress = false; - deferred.reject(e); + deferred?.reject(e); }); - return deferred.promise; + return deferred!.promise; } private async processQueryResponseForUser( userId: string, - dkResponse: IDownloadKeyResult["device_keys"]["user_id"], + dkResponse: DeviceKeys, crossSigningResponse: { - master: IDownloadKeyResult["master_keys"]["user_id"]; - self_signing: IDownloadKeyResult["master_keys"]["user_id"]; // eslint-disable-line camelcase - user_signing: IDownloadKeyResult["user_signing_keys"]["user_id"]; // eslint-disable-line camelcase + master?: Keys; + self_signing?: SigningKeys; + user_signing?: SigningKeys; }, ): Promise { logger.log('got device keys for ' + userId + ':', dkResponse); @@ -840,7 +837,7 @@ class DeviceListUpdateSerialiser { await updateStoredDeviceKeysForUser( this.olmDevice, userId, userStore, dkResponse || {}, - this.baseApis.getUserId(), this.baseApis.deviceId, + this.baseApis.getUserId()!, this.baseApis.deviceId!, ); // put the updates into the object that will be returned as our results diff --git a/src/crypto/OlmDevice.ts b/src/crypto/OlmDevice.ts index 8cd7b3552f1..2049a82de26 100644 --- a/src/crypto/OlmDevice.ts +++ b/src/crypto/OlmDevice.ts @@ -15,9 +15,8 @@ limitations under the License. */ import { Account, InboundGroupSession, OutboundGroupSession, Session, Utility } from "@matrix-org/olm"; -import { Logger } from "loglevel"; -import { logger } from '../logger'; +import { logger, PrefixedLogger } from '../logger'; import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store'; import * as algorithms from './algorithms'; import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base"; @@ -121,9 +120,9 @@ interface IInboundGroupSessionKey { chain_index: number; key: string; forwarding_curve25519_key_chain: string[]; - sender_claimed_ed25519_key: string; + sender_claimed_ed25519_key: string | null; shared_history: boolean; - untrusted: boolean; + untrusted?: boolean; } /* eslint-enable camelcase */ @@ -145,9 +144,9 @@ export class OlmDevice { public pickleKey = "DEFAULT_KEY"; // set by consumers // don't know these until we load the account from storage in init() - public deviceCurve25519Key: string = null; - public deviceEd25519Key: string = null; - private maxOneTimeKeys: number = null; + public deviceCurve25519Key: string | null = null; + public deviceEd25519Key: string | null = null; + private maxOneTimeKeys: number | null = null; // we don't bother stashing outboundgroupsessions in the cryptoStore - // instead we keep them here. @@ -266,8 +265,8 @@ export class OlmDevice { lastReceivedMessageTs: session.lastReceivedMessageTs, }; this.cryptoStore.storeEndToEndSession( - deviceKey, - sessionId, + deviceKey!, + sessionId!, sessionInfo, txn, ); @@ -358,7 +357,7 @@ export class OlmDevice { // is not exactly the same thing you get in method _getSession // see documentation of IndexedDBCryptoStore.getAllEndToEndSessions this.cryptoStore.getAllEndToEndSessions(txn, (pickledSession) => { - result.sessions.push(pickledSession); + result.sessions!.push(pickledSession!); }); }, ); @@ -384,8 +383,8 @@ export class OlmDevice { func: (unpickledSessionInfo: IUnpickledSessionInfo) => void, ): void { this.cryptoStore.getEndToEndSession( - deviceKey, sessionId, txn, (sessionInfo: ISessionInfo) => { - this.unpickleSession(sessionInfo, func); + deviceKey, sessionId, txn, (sessionInfo: ISessionInfo | null) => { + this.unpickleSession(sessionInfo!, func); }, ); } @@ -405,7 +404,7 @@ export class OlmDevice { ): void { const session = new global.Olm.Session(); try { - session.unpickle(this.pickleKey, sessionInfo.session); + session.unpickle(this.pickleKey, sessionInfo.session!); const unpickledSessInfo: IUnpickledSessionInfo = Object.assign({}, sessionInfo, { session }); func(unpickledSessInfo); @@ -491,7 +490,7 @@ export class OlmDevice { * @return {number} number of keys */ public maxNumberOfOneTimeKeys(): number { - return this.maxOneTimeKeys; + return this.maxOneTimeKeys ?? -1; } /** @@ -554,7 +553,7 @@ export class OlmDevice { }); }, ); - return result; + return result!; } public async forgetOldFallbackKey(): Promise { @@ -607,7 +606,7 @@ export class OlmDevice { }, logger.withPrefix("[createOutboundSession]"), ); - return newSessionId; + return newSessionId!; } /** @@ -668,7 +667,7 @@ export class OlmDevice { logger.withPrefix("[createInboundSession]"), ); - return result; + return result!; } /** @@ -703,7 +702,7 @@ export class OlmDevice { log, ); - return sessionIds; + return sessionIds!; } /** @@ -714,13 +713,13 @@ export class OlmDevice { * @param {boolean} nowait Don't wait for an in-progress session to complete. * This should only be set to true of the calling function is the function * that marked the session as being in-progress. - * @param {Logger} [log] A possibly customised log + * @param {PrefixedLogger} [log] A possibly customised log * @return {Promise} session id, or null if no established session */ public async getSessionIdForDevice( theirDeviceIdentityKey: string, nowait = false, - log?: Logger, + log?: PrefixedLogger, ): Promise { const sessionInfos = await this.getSessionInfoForDevice(theirDeviceIdentityKey, nowait, log); @@ -780,7 +779,11 @@ export class OlmDevice { // return an empty result } } - const info = []; + const info: { + lastReceivedMessageTs: number; + hasReceivedMessage: boolean; + sessionId: string; + }[] = []; await this.cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_SESSIONS], @@ -790,9 +793,9 @@ export class OlmDevice { for (const sessionId of sessionIds) { this.unpickleSession(sessions[sessionId], (sessInfo: IUnpickledSessionInfo) => { info.push({ - lastReceivedMessageTs: sessInfo.lastReceivedMessageTs, + lastReceivedMessageTs: sessInfo.lastReceivedMessageTs!, hasReceivedMessage: sessInfo.session.has_received_message(), - sessionId: sessionId, + sessionId, }); }); } @@ -801,7 +804,7 @@ export class OlmDevice { log, ); - return info; + return info!; } /** @@ -916,7 +919,7 @@ export class OlmDevice { await this.cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed); } - public sessionMayHaveProblems(deviceKey: string, timestamp: number): Promise { + public sessionMayHaveProblems(deviceKey: string, timestamp: number): Promise { return this.cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp); } @@ -1056,10 +1059,14 @@ export class OlmDevice { senderKey: string, sessionId: string, txn: unknown, - func: (session: InboundGroupSession, data: InboundGroupSessionData, withheld?: IWithheld) => void, + func: ( + session: InboundGroupSession | null, + data: InboundGroupSessionData | null, + withheld: IWithheld | null, + ) => void, ): void { this.cryptoStore.getEndToEndInboundGroupSession( - senderKey, sessionId, txn, (sessionData: InboundGroupSessionData, withheld: IWithheld | null) => { + senderKey, sessionId, txn, (sessionData: InboundGroupSessionData | null, withheld: IWithheld | null) => { if (sessionData === null) { func(null, null, withheld); return; @@ -1112,94 +1119,94 @@ export class OlmDevice { IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS, ], (txn) => { /* if we already have this session, consider updating it */ - this.getInboundGroupSession( - roomId, senderKey, sessionId, txn, - (existingSession: InboundGroupSession, existingSessionData: InboundGroupSessionData) => { - // new session. - const session = new global.Olm.InboundGroupSession(); - try { - if (exportFormat) { - session.import_session(sessionKey); - } else { - session.create(sessionKey); - } - if (sessionId != session.session_id()) { - throw new Error( - "Mismatched group session ID from senderKey: " + - senderKey, - ); - } + this.getInboundGroupSession(roomId, senderKey, sessionId, txn, ( + existingSession: InboundGroupSession | null, + existingSessionData: InboundGroupSessionData | null, + ) => { + // new session. + const session = new global.Olm.InboundGroupSession(); + try { + if (exportFormat) { + session.import_session(sessionKey); + } else { + session.create(sessionKey); + } + if (sessionId != session.session_id()) { + throw new Error( + "Mismatched group session ID from senderKey: " + + senderKey, + ); + } - if (existingSession) { - logger.log( - "Update for megolm session " - + senderKey + "/" + sessionId, - ); - if (existingSession.first_known_index() <= session.first_known_index()) { - if (!existingSessionData.untrusted || extraSessionData.untrusted) { - // existing session has less-than-or-equal index - // (i.e. can decrypt at least as much), and the - // new session's trust does not win over the old - // session's trust, so keep it - logger.log(`Keeping existing megolm session ${sessionId}`); - return; - } - if (existingSession.first_known_index() < session.first_known_index()) { - // We want to upgrade the existing session's trust, - // but we can't just use the new session because we'll - // lose the lower index. Check that the sessions connect - // properly, and then manually set the existing session - // as trusted. - if ( - existingSession.export_session(session.first_known_index()) - === session.export_session(session.first_known_index()) - ) { - logger.info( - "Upgrading trust of existing megolm session " + - sessionId + " based on newly-received trusted session", - ); - existingSessionData.untrusted = false; - this.cryptoStore.storeEndToEndInboundGroupSession( - senderKey, sessionId, existingSessionData, txn, - ); - } else { - logger.warn( - "Newly-received megolm session " + sessionId + - " does not match existing session! Keeping existing session", - ); - } - return; + if (existingSession) { + logger.log( + "Update for megolm session " + + senderKey + "/" + sessionId, + ); + if (existingSession.first_known_index() <= session.first_known_index()) { + if (!existingSessionData!.untrusted || extraSessionData.untrusted) { + // existing session has less-than-or-equal index + // (i.e. can decrypt at least as much), and the + // new session's trust does not win over the old + // session's trust, so keep it + logger.log(`Keeping existing megolm session ${sessionId}`); + return; + } + if (existingSession.first_known_index() < session.first_known_index()) { + // We want to upgrade the existing session's trust, + // but we can't just use the new session because we'll + // lose the lower index. Check that the sessions connect + // properly, and then manually set the existing session + // as trusted. + if ( + existingSession.export_session(session.first_known_index()) + === session.export_session(session.first_known_index()) + ) { + logger.info( + "Upgrading trust of existing megolm session " + + sessionId + " based on newly-received trusted session", + ); + existingSessionData!.untrusted = false; + this.cryptoStore.storeEndToEndInboundGroupSession( + senderKey, sessionId, existingSessionData!, txn, + ); + } else { + logger.warn( + "Newly-received megolm session " + sessionId + + " does not match existing session! Keeping existing session", + ); } - // If the sessions have the same index, go ahead and store the new trusted one. + return; } + // If the sessions have the same index, go ahead and store the new trusted one. } + } - logger.info( - "Storing megolm session " + senderKey + "/" + sessionId + - " with first index " + session.first_known_index(), - ); + logger.info( + "Storing megolm session " + senderKey + "/" + sessionId + + " with first index " + session.first_known_index(), + ); - const sessionData = Object.assign({}, extraSessionData, { - room_id: roomId, - session: session.pickle(this.pickleKey), - keysClaimed: keysClaimed, - forwardingCurve25519KeyChain: forwardingCurve25519KeyChain, - }); + const sessionData = Object.assign({}, extraSessionData, { + room_id: roomId, + session: session.pickle(this.pickleKey), + keysClaimed: keysClaimed, + forwardingCurve25519KeyChain: forwardingCurve25519KeyChain, + }); - this.cryptoStore.storeEndToEndInboundGroupSession( - senderKey, sessionId, sessionData, txn, - ); + this.cryptoStore.storeEndToEndInboundGroupSession( + senderKey, sessionId, sessionData, txn, + ); - if (!existingSession && extraSessionData.sharedHistory) { - this.cryptoStore.addSharedHistoryInboundGroupSession( - roomId, senderKey, sessionId, txn, - ); - } - } finally { - session.free(); + if (!existingSession && extraSessionData.sharedHistory) { + this.cryptoStore.addSharedHistoryInboundGroupSession( + roomId, senderKey, sessionId, txn, + ); } - }, - ); + } finally { + session.free(); + } + }); }, logger.withPrefix("[addInboundGroupSession]"), ); @@ -1261,7 +1268,7 @@ export class OlmDevice { eventId: string, timestamp: number, ): Promise { - let result: IDecryptedGroupMessage; + let result: IDecryptedGroupMessage | null = null; // when the localstorage crypto store is used as an indexeddb backend, // exceptions thrown from within the inner function are not passed through // to the top level, so we store exceptions in a variable and raise them at @@ -1275,7 +1282,7 @@ export class OlmDevice { ], (txn) => { this.getInboundGroupSession( roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => { - if (session === null) { + if (session === null || sessionData === null) { if (withheld) { error = new algorithms.DecryptionError( "MEGOLM_UNKNOWN_INBOUND_SESSION_ID", @@ -1292,7 +1299,7 @@ export class OlmDevice { try { res = session.decrypt(body); } catch (e) { - if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX' && withheld) { + if ((e)?.message === 'OLM.UNKNOWN_MESSAGE_INDEX' && withheld) { error = new algorithms.DecryptionError( "MEGOLM_UNKNOWN_INBOUND_SESSION_ID", calculateWithheldMessage(withheld), @@ -1301,7 +1308,7 @@ export class OlmDevice { }, ); } else { - error = e; + error = e; } return; } @@ -1350,7 +1357,7 @@ export class OlmDevice { forwardingCurve25519KeyChain: ( sessionData.forwardingCurve25519KeyChain || [] ), - untrusted: sessionData.untrusted, + untrusted: !!sessionData.untrusted, }; }, ); @@ -1358,10 +1365,10 @@ export class OlmDevice { logger.withPrefix("[decryptGroupMessage]"), ); - if (error) { + if (error!) { throw error; } - return result; + return result!; } /** @@ -1404,7 +1411,7 @@ export class OlmDevice { logger.withPrefix("[hasInboundSessionKeys]"), ); - return result; + return result!; } /** @@ -1431,8 +1438,8 @@ export class OlmDevice { senderKey: string, sessionId: string, chainIndex?: number, - ): Promise { - let result: IInboundGroupSessionKey; + ): Promise { + let result: IInboundGroupSessionKey | null = null; await this.cryptoStore.doTxn( 'readonly', [ IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, @@ -1440,7 +1447,7 @@ export class OlmDevice { ], (txn) => { this.getInboundGroupSession( roomId, senderKey, sessionId, txn, (session, sessionData) => { - if (session === null) { + if (session === null || sessionData === null) { result = null; return; } @@ -1520,7 +1527,7 @@ export class OlmDevice { }, logger.withPrefix("[getSharedHistoryInboundGroupSessionsForRoom]"), ); - return result; + return result!; } // Utilities diff --git a/src/crypto/SecretStorage.ts b/src/crypto/SecretStorage.ts index 9c5a9faf122..d0fb7f90e40 100644 --- a/src/crypto/SecretStorage.ts +++ b/src/crypto/SecretStorage.ts @@ -182,7 +182,7 @@ export class SecretStorage { * the form [keyId, keyInfo]. Otherwise, null is returned. * XXX: why is this an array when addKey returns an object? */ - public async getKey(keyId: string): Promise { + public async getKey(keyId?: string | null): Promise { if (!keyId) { keyId = await this.getDefaultKeyId(); } @@ -237,7 +237,7 @@ export class SecretStorage { * @param {Array} keys The IDs of the keys to use to encrypt the secret * or null/undefined to use the default key. */ - public async store(name: string, secret: string, keys?: string[]): Promise { + public async store(name: string, secret: string, keys?: string[] | null): Promise { const encrypted: Record = {}; if (!keys) { @@ -284,7 +284,7 @@ export class SecretStorage { * * @return {string} the contents of the secret */ - public async get(name: string): Promise { + public async get(name: string): Promise { const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name); if (!secretInfo) { return; diff --git a/src/crypto/algorithms/base.ts b/src/crypto/algorithms/base.ts index 070796720f1..506a27ee810 100644 --- a/src/crypto/algorithms/base.ts +++ b/src/crypto/algorithms/base.ts @@ -242,7 +242,7 @@ export abstract class DecryptionAlgorithm { export class DecryptionError extends Error { public readonly detailedString: string; - constructor(public readonly code: string, msg: string, details?: Record) { + constructor(public readonly code: string, msg: string, details?: Record) { super(msg); this.code = code; this.name = 'DecryptionError'; @@ -250,7 +250,7 @@ export class DecryptionError extends Error { } } -function detailedStringForDecryptionError(err: DecryptionError, details?: Record): string { +function detailedStringForDecryptionError(err: DecryptionError, details?: Record): string { let result = err.name + '[msg: ' + err.message; if (details) { @@ -272,7 +272,11 @@ function detailedStringForDecryptionError(err: DecryptionError, details?: Record * @extends Error */ export class UnknownDeviceError extends Error { - constructor(msg: string, public readonly devices: Record>) { + constructor( + msg: string, + public readonly devices: Record>, + public event?: MatrixEvent, + ) { super(msg); this.name = "UnknownDeviceError"; this.devices = devices; @@ -295,7 +299,7 @@ export class UnknownDeviceError extends Error { export function registerAlgorithm( algorithm: string, encryptor: new (params: IParams) => EncryptionAlgorithm, - decryptor: new (params: Omit) => DecryptionAlgorithm, + decryptor: new (params: DecryptionClassParams) => DecryptionAlgorithm, ): void { ENCRYPTION_CLASSES.set(algorithm, encryptor); DECRYPTION_CLASSES.set(algorithm, decryptor); diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index 5c358950a75..e91213185fa 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -40,6 +40,7 @@ import { EventType, MsgType } from '../../@types/event'; import { IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index"; import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager'; import { OlmGroupSessionExtraData } from "../../@types/crypto"; +import { MatrixError } from "../../http-api"; // determine whether the key can be shared with invitees export function isRoomSharedHistory(room: Room): boolean { @@ -492,13 +493,13 @@ class MegolmEncryption extends EncryptionAlgorithm { const key = this.olmDevice.getOutboundGroupSessionKey(sessionId); await this.olmDevice.addInboundGroupSession( - this.roomId, this.olmDevice.deviceCurve25519Key, [], sessionId, - key.key, { ed25519: this.olmDevice.deviceEd25519Key }, false, + this.roomId, this.olmDevice.deviceCurve25519Key!, [], sessionId, + key.key, { ed25519: this.olmDevice.deviceEd25519Key! }, false, { sharedHistory }, ); // don't wait for it to complete - this.crypto.backupManager.backupGroupSession(this.olmDevice.deviceCurve25519Key, sessionId); + this.crypto.backupManager.backupGroupSession(this.olmDevice.deviceCurve25519Key!, sessionId); return new OutboundSessionInfo(sessionId, sharedHistory); } @@ -929,7 +930,7 @@ class MegolmEncryption extends EncryptionAlgorithm { room_id: this.roomId, session_id: session.sessionId, algorithm: olmlib.MEGOLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key, + sender_key: this.olmDevice.deviceCurve25519Key!, }; const userDeviceMaps = this.splitDevices(devicesByUser); @@ -1259,21 +1260,21 @@ class MegolmDecryption extends DecryptionAlgorithm { // (fixes https://github.com/vector-im/element-web/issues/5001) this.addEventToPendingList(event); - let res: IDecryptedGroupMessage; + let res: IDecryptedGroupMessage | null; try { res = await this.olmDevice.decryptGroupMessage( - event.getRoomId(), content.sender_key, content.session_id, content.ciphertext, + event.getRoomId()!, content.sender_key, content.session_id, content.ciphertext, event.getId(), event.getTs(), ); } catch (e) { - if (e.name === "DecryptionError") { + if ((e).name === "DecryptionError") { // re-throw decryption errors as-is throw e; } let errorCode = "OLM_DECRYPT_GROUP_MESSAGE_ERROR"; - if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX') { + if ((e)?.message === 'OLM.UNKNOWN_MESSAGE_INDEX') { this.requestKeysForEvent(event); errorCode = 'OLM_UNKNOWN_MESSAGE_INDEX'; @@ -1363,7 +1364,7 @@ class MegolmDecryption extends DecryptionAlgorithm { const recipients = event.getKeyRequestRecipients(this.userId); this.crypto.requestRoomKey({ - room_id: event.getRoomId(), + room_id: event.getRoomId()!, algorithm: wireContent.algorithm, sender_key: wireContent.sender_key, session_id: wireContent.session_id, @@ -1384,7 +1385,7 @@ class MegolmDecryption extends DecryptionAlgorithm { if (!this.pendingEvents.has(senderKey)) { this.pendingEvents.set(senderKey, new Map>()); } - const senderPendingEvents = this.pendingEvents.get(senderKey); + const senderPendingEvents = this.pendingEvents.get(senderKey)!; if (!senderPendingEvents.has(sessionId)) { senderPendingEvents.set(sessionId, new Set()); } @@ -1410,9 +1411,9 @@ class MegolmDecryption extends DecryptionAlgorithm { pendingEvents.delete(event); if (pendingEvents.size === 0) { - senderPendingEvents.delete(sessionId); + senderPendingEvents!.delete(sessionId); } - if (senderPendingEvents.size === 0) { + if (senderPendingEvents!.size === 0) { this.pendingEvents.delete(senderKey); } } @@ -1424,7 +1425,7 @@ class MegolmDecryption extends DecryptionAlgorithm { */ public async onRoomKeyEvent(event: MatrixEvent): Promise { const content = event.getContent>(); - let senderKey = event.getSenderKey(); + let senderKey = event.getSenderKey()!; let forwardingKeyChain: string[] = []; let exportFormat = false; let keysClaimed: ReturnType; @@ -1454,7 +1455,7 @@ class MegolmDecryption extends DecryptionAlgorithm { olmlib.OLM_ALGORITHM, senderKey, ); - const senderKeyUser = this.baseApis.crypto.deviceList.getUserByIdentityKey( + const senderKeyUser = this.baseApis.crypto!.deviceList.getUserByIdentityKey( olmlib.OLM_ALGORITHM, senderKey, ); @@ -1533,13 +1534,16 @@ class MegolmDecryption extends DecryptionAlgorithm { await this.crypto.cryptoStore.doTxn( 'readwrite', ['parked_shared_history'], - (txn) => this.crypto.cryptoStore.addParkedSharedHistory(content.room_id, parkedData, txn), + (txn) => this.crypto.cryptoStore.addParkedSharedHistory(content.room_id!, parkedData, txn), logger.withPrefix("[addParkedSharedHistory]"), ); return; } - const sendingDevice = this.crypto.deviceList.getDeviceByIdentityKey(olmlib.OLM_ALGORITHM, senderKey); + const sendingDevice = this.crypto.deviceList.getDeviceByIdentityKey( + olmlib.OLM_ALGORITHM, + senderKey, + ) ?? undefined; const deviceTrust = this.crypto.checkDeviceInfoTrust(event.getSender(), sendingDevice); if (fromUs && !deviceTrust.isVerified()) { @@ -1698,7 +1702,7 @@ class MegolmDecryption extends DecryptionAlgorithm { public shareKeysWithDevice(keyRequest: IncomingRoomKeyRequest): void { const userId = keyRequest.userId; const deviceId = keyRequest.deviceId; - const deviceInfo = this.crypto.getStoredDevice(userId, deviceId); + const deviceInfo = this.crypto.getStoredDevice(userId, deviceId)!; const body = keyRequest.requestBody; this.olmlib.ensureOlmSessionsForDevices( @@ -1739,7 +1743,7 @@ class MegolmDecryption extends DecryptionAlgorithm { this.olmDevice, userId, deviceInfo, - payload, + payload!, ).then(() => { const contentMap = { [userId]: { @@ -1766,12 +1770,12 @@ class MegolmDecryption extends DecryptionAlgorithm { "algorithm": olmlib.MEGOLM_ALGORITHM, "room_id": roomId, "sender_key": senderKey, - "sender_claimed_ed25519_key": key.sender_claimed_ed25519_key, + "sender_claimed_ed25519_key": key!.sender_claimed_ed25519_key!, "session_id": sessionId, - "session_key": key.key, - "chain_index": key.chain_index, - "forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain, - "org.matrix.msc3061.shared_history": key.shared_history || false, + "session_key": key!.key, + "chain_index": key!.chain_index, + "forwarding_curve25519_key_chain": key!.forwarding_curve25519_key_chain, + "org.matrix.msc3061.shared_history": key!.shared_history || false, }, }; } @@ -1901,7 +1905,7 @@ class MegolmDecryption extends DecryptionAlgorithm { for (const deviceInfo of devices) { const encryptedContent: IEncryptedContent = { algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key, + sender_key: this.olmDevice.deviceCurve25519Key!, ciphertext: {}, }; contentMap[userId][deviceInfo.deviceId] = encryptedContent; diff --git a/src/crypto/algorithms/olm.ts b/src/crypto/algorithms/olm.ts index 14ee7516a90..c85fdf9c89d 100644 --- a/src/crypto/algorithms/olm.ts +++ b/src/crypto/algorithms/olm.ts @@ -180,14 +180,14 @@ class OlmDecryption extends DecryptionAlgorithm { ); } - if (!(this.olmDevice.deviceCurve25519Key in ciphertext)) { + if (!(this.olmDevice.deviceCurve25519Key! in ciphertext)) { throw new DecryptionError( "OLM_NOT_INCLUDED_IN_RECIPIENTS", "Not included in recipients", ); } - const message = ciphertext[this.olmDevice.deviceCurve25519Key]; - let payloadString; + const message = ciphertext[this.olmDevice.deviceCurve25519Key!]; + let payloadString: string; try { payloadString = await this.decryptMessage(deviceKey, message); @@ -196,7 +196,7 @@ class OlmDecryption extends DecryptionAlgorithm { "OLM_BAD_ENCRYPTED_MESSAGE", "Bad Encrypted Message", { sender: deviceKey, - err: e, + err: e as Error, }, ); } @@ -217,7 +217,7 @@ class OlmDecryption extends DecryptionAlgorithm { "OLM_BAD_RECIPIENT_KEY", "Message not intended for this device", { intended: payload.recipient_keys.ed25519, - our_key: this.olmDevice.deviceEd25519Key, + our_key: this.olmDevice.deviceEd25519Key!, }, ); } @@ -233,7 +233,7 @@ class OlmDecryption extends DecryptionAlgorithm { olmlib.OLM_ALGORITHM, deviceKey, ); - if (senderKeyUser !== event.getSender() && senderKeyUser !== undefined) { + if (senderKeyUser !== event.getSender() && senderKeyUser != undefined) { throw new DecryptionError( "OLM_BAD_SENDER", "Message claimed to be from " + event.getSender(), { @@ -325,13 +325,13 @@ class OlmDecryption extends DecryptionAlgorithm { // session, so it should have worked. throw new Error( "Error decrypting prekey message with existing session id " + - sessionId + ": " + e.message, + sessionId + ": " + (e).message, ); } // otherwise it's probably a message for another session; carry on, but // keep a record of the error - decryptionErrors[sessionId] = e.message; + decryptionErrors[sessionId] = (e).message; } } @@ -358,7 +358,7 @@ class OlmDecryption extends DecryptionAlgorithm { theirDeviceIdentityKey, message.type, message.body, ); } catch (e) { - decryptionErrors["(new)"] = e.message; + decryptionErrors["(new)"] = (e).message; throw new Error( "Error decrypting prekey message: " + JSON.stringify(decryptionErrors), diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index 6722aa363ff..be5c9e0aa94 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -40,6 +40,7 @@ import { import { UnstableValue } from "../NamespacedValue"; import { CryptoEvent, IMegolmSessionData } from "./index"; import { crypto } from "./crypto"; +import { HTTPError, MatrixError } from "../http-api"; const KEY_BACKUP_KEYS_PER_REQUEST = 200; const KEY_BACKUP_CHECK_RATE_LIMIT = 5000; // ms @@ -62,7 +63,7 @@ export type TrustInfo = { }; export interface IKeyBackupCheck { - backupInfo: IKeyBackupInfo; + backupInfo?: IKeyBackupInfo; trustInfo: TrustInfo; } @@ -85,9 +86,7 @@ interface BackupAlgorithmClass { init(authData: AuthData, getKey: GetKey): Promise; // prepare a brand new backup - prepare( - key: string | Uint8Array | null, - ): Promise<[Uint8Array, AuthData]>; + prepare(key?: string | Uint8Array | null): Promise<[Uint8Array, AuthData]>; checkBackupVersion(info: IKeyBackupInfo): void; } @@ -221,19 +220,19 @@ export class BackupManager { * one of the user's verified devices, start backing up * to it. */ - public async checkAndStart(): Promise { + public async checkAndStart(): Promise { logger.log("Checking key backup status..."); if (this.baseApis.isGuest()) { logger.log("Skipping key backup check since user is guest"); this.checkedForBackup = true; return null; } - let backupInfo: IKeyBackupInfo; + let backupInfo: IKeyBackupInfo | undefined; try { - backupInfo = await this.baseApis.getKeyBackupVersion(); + backupInfo = await this.baseApis.getKeyBackupVersion() ?? undefined; } catch (e) { logger.log("Error checking for active key backup", e); - if (e.httpStatus === 404) { + if ((e).httpStatus === 404) { // 404 is returned when the key backup does not exist, so that // counts as successfully checking. this.checkedForBackup = true; @@ -245,11 +244,8 @@ export class BackupManager { const trustInfo = await this.isKeyBackupTrusted(backupInfo); if (trustInfo.usable && !this.backupInfo) { - logger.log( - "Found usable key backup v" + backupInfo.version + - ": enabling key backups", - ); - await this.enableKeyBackup(backupInfo); + logger.log(`Found usable key backup v${backupInfo!.version}: enabling key backups`); + await this.enableKeyBackup(backupInfo!); } else if (!trustInfo.usable && this.backupInfo) { logger.log("No usable key backup: disabling key backup"); this.disableKeyBackup(); @@ -257,13 +253,11 @@ export class BackupManager { logger.log("No usable key backup: not enabling key backup"); } else if (trustInfo.usable && this.backupInfo) { // may not be the same version: if not, we should switch - if (backupInfo.version !== this.backupInfo.version) { - logger.log( - "On backup version " + this.backupInfo.version + " but found " + - "version " + backupInfo.version + ": switching.", - ); + if (backupInfo!.version !== this.backupInfo.version) { + logger.log(`On backup version ${this.backupInfo.version} but ` + + `found version ${backupInfo!.version}: switching.`); this.disableKeyBackup(); - await this.enableKeyBackup(backupInfo); + await this.enableKeyBackup(backupInfo!); // We're now using a new backup, so schedule all the keys we have to be // uploaded to the new backup. This is a bit of a workaround to upload // keys to a new backup in *most* cases, but it won't cover all cases @@ -271,7 +265,7 @@ export class BackupManager { // see https://github.com/vector-im/element-web/issues/14833 await this.scheduleAllGroupSessionsForBackup(); } else { - logger.log("Backup version " + backupInfo.version + " still current"); + logger.log(`Backup version ${backupInfo!.version} still current`); } } @@ -287,7 +281,7 @@ export class BackupManager { * trust information (as returned by isKeyBackupTrusted) * in trustInfo. */ - public async checkKeyBackup(): Promise { + public async checkKeyBackup(): Promise { this.checkedForBackup = false; return this.checkAndStart(); } @@ -325,7 +319,7 @@ export class BackupManager { * ] * } */ - public async isKeyBackupTrusted(backupInfo: IKeyBackupInfo): Promise { + public async isKeyBackupTrusted(backupInfo?: IKeyBackupInfo): Promise { const ret = { usable: false, trusted_locally: false, @@ -342,9 +336,10 @@ export class BackupManager { return ret; } - const privKey = await this.baseApis.crypto.getSessionBackupPrivateKey(); + const userId = this.baseApis.getUserId()!; + const privKey = await this.baseApis.crypto!.getSessionBackupPrivateKey(); if (privKey) { - let algorithm; + let algorithm: BackupAlgorithm | null = null; try { algorithm = await BackupManager.makeAlgorithm(backupInfo, async () => privKey); @@ -356,13 +351,11 @@ export class BackupManager { // do nothing -- if we have an error, then we don't mark it as // locally trusted } finally { - if (algorithm) { - algorithm.free(); - } + algorithm?.free(); } } - const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || {}; + const mySigs = backupInfo.auth_data.signatures[userId] || {}; for (const keyId of Object.keys(mySigs)) { const keyIdParts = keyId.split(':'); @@ -375,14 +368,14 @@ export class BackupManager { const sigInfo: SigInfo = { deviceId: keyIdParts[1] }; // first check to see if it's from our cross-signing key - const crossSigningId = this.baseApis.crypto.crossSigningInfo.getId(); + const crossSigningId = this.baseApis.crypto!.crossSigningInfo.getId(); if (crossSigningId === sigInfo.deviceId) { sigInfo.crossSigningId = true; try { await verifySignature( - this.baseApis.crypto.olmDevice, + this.baseApis.crypto!.olmDevice, backupInfo.auth_data, - this.baseApis.getUserId(), + userId, sigInfo.deviceId, crossSigningId, ); @@ -400,17 +393,16 @@ export class BackupManager { // Now look for a sig from a device // At some point this can probably go away and we'll just support // it being signed by the cross-signing master key - const device = this.baseApis.crypto.deviceList.getStoredDevice( - this.baseApis.getUserId(), sigInfo.deviceId, + const device = this.baseApis.crypto!.deviceList.getStoredDevice(userId, sigInfo.deviceId, ); if (device) { sigInfo.device = device; - sigInfo.deviceTrust = this.baseApis.checkDeviceTrust(this.baseApis.getUserId(), sigInfo.deviceId); + sigInfo.deviceTrust = this.baseApis.checkDeviceTrust(userId, sigInfo.deviceId); try { await verifySignature( - this.baseApis.crypto.olmDevice, + this.baseApis.crypto!.olmDevice, backupInfo.auth_data, - this.baseApis.getUserId(), + userId, device.deviceId, device.getFingerprint(), ); @@ -431,12 +423,7 @@ export class BackupManager { } ret.usable = ret.sigs.some((s) => { - return ( - s.valid && ( - (s.device && s.deviceTrust.isVerified()) || - (s.crossSigningId) - ) - ); + return s.valid && ((s.device && s.deviceTrust?.isVerified()) || (s.crossSigningId)); }); return ret; } @@ -474,17 +461,17 @@ export class BackupManager { } catch (err) { numFailures++; logger.log("Key backup request failed", err); - if (err.data) { + if ((err).data) { if ( - err.data.errcode == 'M_NOT_FOUND' || - err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION' + (err).data.errcode == 'M_NOT_FOUND' || + (err).data.errcode == 'M_WRONG_ROOM_KEYS_VERSION' ) { // Re-check key backup status on error, so we can be // sure to present the current situation when asked. await this.checkKeyBackup(); // Backup version has changed or this backup version // has been deleted - this.baseApis.crypto.emit(CryptoEvent.KeyBackupFailed, err.data.errcode); + this.baseApis.crypto!.emit(CryptoEvent.KeyBackupFailed, (err).data.errcode!); throw err; } } @@ -507,50 +494,50 @@ export class BackupManager { * @returns {number} Number of sessions backed up */ public async backupPendingKeys(limit: number): Promise { - const sessions = await this.baseApis.crypto.cryptoStore.getSessionsNeedingBackup(limit); + const sessions = await this.baseApis.crypto!.cryptoStore.getSessionsNeedingBackup(limit); if (!sessions.length) { return 0; } - let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); - this.baseApis.crypto.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining); + let remaining = await this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup(); + this.baseApis.crypto!.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining); const rooms: IKeyBackup["rooms"] = {}; for (const session of sessions) { - const roomId = session.sessionData.room_id; + const roomId = session.sessionData!.room_id; if (rooms[roomId] === undefined) { rooms[roomId] = { sessions: {} }; } - const sessionData = this.baseApis.crypto.olmDevice.exportInboundGroupSession( - session.senderKey, session.sessionId, session.sessionData, + const sessionData = this.baseApis.crypto!.olmDevice.exportInboundGroupSession( + session.senderKey, session.sessionId, session.sessionData!, ); sessionData.algorithm = MEGOLM_ALGORITHM; const forwardedCount = (sessionData.forwarding_curve25519_key_chain || []).length; - const userId = this.baseApis.crypto.deviceList.getUserByIdentityKey( + const userId = this.baseApis.crypto!.deviceList.getUserByIdentityKey( MEGOLM_ALGORITHM, session.senderKey, ); - const device = this.baseApis.crypto.deviceList.getDeviceByIdentityKey( + const device = this.baseApis.crypto!.deviceList.getDeviceByIdentityKey( MEGOLM_ALGORITHM, session.senderKey, - ); - const verified = this.baseApis.crypto.checkDeviceInfoTrust(userId, device).isVerified(); + ) ?? undefined; + const verified = this.baseApis.crypto!.checkDeviceInfoTrust(userId!, device).isVerified(); rooms[roomId]['sessions'][session.sessionId] = { first_message_index: sessionData.first_known_index, forwarded_count: forwardedCount, is_verified: verified, - session_data: await this.algorithm.encryptSession(sessionData), + session_data: await this.algorithm!.encryptSession(sessionData), }; } - await this.baseApis.sendKeyBackup(undefined, undefined, this.backupInfo.version, { rooms }); + await this.baseApis.sendKeyBackup(undefined, undefined, this.backupInfo!.version, { rooms }); - await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions); - remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); - this.baseApis.crypto.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining); + await this.baseApis.crypto!.cryptoStore.unmarkSessionsNeedingBackup(sessions); + remaining = await this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup(); + this.baseApis.crypto!.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining); return sessions.length; } @@ -558,7 +545,7 @@ export class BackupManager { public async backupGroupSession( senderKey: string, sessionId: string, ): Promise { - await this.baseApis.crypto.cryptoStore.markSessionsNeedingBackup([{ + await this.baseApis.crypto!.cryptoStore.markSessionsNeedingBackup([{ senderKey: senderKey, sessionId: sessionId, }]); @@ -590,22 +577,22 @@ export class BackupManager { * (which will be equal to the number of sessions in the store). */ public async flagAllGroupSessionsForBackup(): Promise { - await this.baseApis.crypto.cryptoStore.doTxn( + await this.baseApis.crypto!.cryptoStore.doTxn( 'readwrite', [ IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS, IndexedDBCryptoStore.STORE_BACKUP, ], (txn) => { - this.baseApis.crypto.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => { + this.baseApis.crypto!.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => { if (session !== null) { - this.baseApis.crypto.cryptoStore.markSessionsNeedingBackup([session], txn); + this.baseApis.crypto!.cryptoStore.markSessionsNeedingBackup([session], txn); } }); }, ); - const remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); + const remaining = await this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup(); this.baseApis.emit(CryptoEvent.KeyBackupSessionsRemaining, remaining); return remaining; } @@ -615,7 +602,7 @@ export class BackupManager { * @returns {Promise} Resolves to the number of sessions requiring backup */ public countSessionsNeedingBackup(): Promise { - return this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup(); + return this.baseApis.crypto!.cryptoStore.countSessionsNeedingBackup(); } } @@ -641,7 +628,7 @@ export class Curve25519 implements BackupAlgorithm { } public static async prepare( - key: string | Uint8Array | null, + key?: string | Uint8Array | null, ): Promise<[Uint8Array, AuthData]> { const decryption = new global.Olm.PkDecryption(); try { @@ -741,7 +728,10 @@ function randomBytes(size: number): Uint8Array { return buf; } -const UNSTABLE_MSC3270_NAME = new UnstableValue(null, "org.matrix.msc3270.v1.aes-hmac-sha2"); +const UNSTABLE_MSC3270_NAME = new UnstableValue( + "m.megolm_backup.v1.aes-hmac-sha2", + "org.matrix.msc3270.v1.aes-hmac-sha2", +); export class Aes256 implements BackupAlgorithm { public static algorithmName = UNSTABLE_MSC3270_NAME.name; @@ -769,7 +759,7 @@ export class Aes256 implements BackupAlgorithm { } public static async prepare( - key: string | Uint8Array | null, + key?: string | Uint8Array | null, ): Promise<[Uint8Array, AuthData]> { let outKey: Uint8Array; const authData: Partial = {}; diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index b06561936b4..5b12159ae5d 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -58,9 +58,9 @@ const oneweek = 7 * 24 * 60 * 60 * 1000; export class DehydrationManager { private inProgress = false; private timeoutId: any; - private key: Uint8Array; - private keyInfo: {[props: string]: any}; - private deviceDisplayName: string; + private key?: Uint8Array; + private keyInfo?: {[props: string]: any}; + private deviceDisplayName?: string; constructor(private readonly crypto: Crypto) { this.getDehydrationKeyFromCache(); @@ -97,7 +97,7 @@ export class DehydrationManager { /** set the key, and queue periodic dehydration to the server in the background */ public async setKeyAndQueueDehydration( key: Uint8Array, keyInfo: {[props: string]: any} = {}, - deviceDisplayName: string = undefined, + deviceDisplayName?: string, ): Promise { const matches = await this.setKey(key, keyInfo, deviceDisplayName); if (!matches) { @@ -108,8 +108,8 @@ export class DehydrationManager { public async setKey( key: Uint8Array, keyInfo: {[props: string]: any} = {}, - deviceDisplayName: string = undefined, - ): Promise { + deviceDisplayName?: string, + ): Promise { if (!key) { // unsetting the key -- cancel any pending dehydration task if (this.timeoutId) { @@ -135,9 +135,9 @@ export class DehydrationManager { // dehydrate a new device. If it's the same, we can keep the same // device. (Assume that keyInfo and deviceDisplayName will be the // same if the key is the same.) - let matches: boolean = this.key && key.length == this.key.length; + let matches: boolean = !!this.key && key.length == this.key.length; for (let i = 0; matches && i < key.length; i++) { - if (key[i] != this.key[i]) { + if (key[i] != this.key![i]) { matches = false; } } @@ -150,7 +150,7 @@ export class DehydrationManager { } /** returns the device id of the newly created dehydrated device */ - public async dehydrateDevice(): Promise { + public async dehydrateDevice(): Promise { if (this.inProgress) { logger.log("Dehydration already in progress -- not starting new dehydration"); return; @@ -164,7 +164,7 @@ export class DehydrationManager { const pickleKey = Buffer.from(this.crypto.olmDevice.pickleKey); // update the crypto store with the timestamp - const key = await encryptAES(encodeBase64(this.key), pickleKey, DEHYDRATION_ALGORITHM); + const key = await encryptAES(encodeBase64(this.key!), pickleKey, DEHYDRATION_ALGORITHM); await this.crypto.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], @@ -174,7 +174,7 @@ export class DehydrationManager { { keyInfo: this.keyInfo, key, - deviceDisplayName: this.deviceDisplayName, + deviceDisplayName: this.deviceDisplayName!, time: Date.now(), }, ); @@ -197,14 +197,14 @@ export class DehydrationManager { account.mark_keys_as_published(); // dehydrate the account and store it on the server - const pickledAccount = account.pickle(new Uint8Array(this.key)); + const pickledAccount = account.pickle(new Uint8Array(this.key!)); const deviceData: {[props: string]: any} = { algorithm: DEHYDRATION_ALGORITHM, account: pickledAccount, }; - if (this.keyInfo.passphrase) { - deviceData.passphrase = this.keyInfo.passphrase; + if (this.keyInfo!.passphrase) { + deviceData.passphrase = this.keyInfo!.passphrase; } logger.log("Uploading account to server"); diff --git a/src/crypto/index.ts b/src/crypto/index.ts index 7c0092bf69e..62258bd3491 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -23,6 +23,7 @@ limitations under the License. import anotherjson from "another-json"; +import type { PkDecryption, PkSigning } from "@matrix-org/olm"; import { EventType } from "../@types/event"; import { TypedReEmitter } from '../ReEmitter'; import { logger } from '../logger'; @@ -274,7 +275,7 @@ export class Crypto extends TypedEventEmitter; /** @@ -399,8 +400,8 @@ export class Crypto extends TypedEventEmitter { + const createSSSS = async (opts: IAddSecretStorageKeyOpts, privateKey?: Uint8Array) => { if (privateKey) { opts.key = privateKey; } @@ -859,7 +860,7 @@ export class Crypto extends TypedEventEmitter { if (!keyInfo.mac) { - const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey( + const key = await this.baseApis.cryptoCallbacks.getSecretStorageKey?.( { keys: { [keyId]: keyInfo } }, "", ); if (key) { @@ -934,7 +935,7 @@ export class Crypto extends TypedEventEmitter { return this.secretStorage.addKey(algorithm, opts, keyID); } - public hasSecretStorageKey(keyID: string): Promise { + public hasSecretStorageKey(keyID?: string): Promise { return this.secretStorage.hasKey(keyID); } - public getSecretStorageKey(keyID?: string): Promise { + public getSecretStorageKey(keyID?: string): Promise { return this.secretStorage.getKey(keyID); } @@ -1078,7 +1080,7 @@ export class Crypto extends TypedEventEmitter { + public getSecret(name: string): Promise { return this.secretStorage.get(name); } @@ -1115,14 +1117,14 @@ export class Crypto extends TypedEventEmitter { return this.baseApis.uploadKeySignatures({ [this.userId]: { - [this.deviceId]: signedDevice, + [this.deviceId]: signedDevice!, }, }).then((response) => { const { failures } = response || {}; @@ -1267,9 +1269,7 @@ export class Crypto extends TypedEventEmitter { + ): Promise { // only upgrade if this is the first cross-signing key that we've seen for // them, and if their cross-signing key isn't already verified const trustLevel = this.crossSigningInfo.checkUserTrust(crossSigningInfo); @@ -1359,7 +1359,7 @@ export class Crypto extends TypedEventEmitter { const userId = this.userId; @@ -1520,7 +1521,7 @@ export class Crypto extends TypedEventEmitter): Promise { + private async storeTrustedSelfKeys(keys: Record | null): Promise { if (keys) { this.crossSigningInfo.setKeys(keys); } else { @@ -1721,9 +1718,7 @@ export class Crypto extends TypedEventEmitter { + const promises: Promise[] = []; - let fallbackJson: Record; + let fallbackJson: Record | undefined; if (this.getNeedsNewFallback()) { fallbackJson = {}; const fallbackKeys = await this.olmDevice.getFallbackKey(); @@ -2045,7 +2040,7 @@ export class Crypto extends TypedEventEmitter { - return this.deviceList.downloadKeys(userIds, forceDownload); + return this.deviceList.downloadKeys(userIds, !!forceDownload); } /** @@ -2114,17 +2109,11 @@ export class Crypto extends TypedEventEmitter, ): Promise { - // get rid of any `undefined`s here so we can just check - // for null rather than null or undefined - if (verified === undefined) verified = null; - if (blocked === undefined) blocked = null; - if (known === undefined) known = null; - // Check if the 'device' is actually a cross signing key // The js-sdk's verification treats cross-signing keys as devices // and so uses this method to mark them verified. @@ -2240,9 +2229,9 @@ export class Crypto extends TypedEventEmitter { + public requestVerification(userId: string, devices?: string[]): Promise { if (!devices) { devices = Object.keys(this.deviceList.getRawStoredDevicesForUser(userId)); } @@ -2597,7 +2586,7 @@ export class Crypto extends TypedEventEmitter = null; + let storeConfigPromise: Promise | null = null; if (!existingConfig) { storeConfigPromise = this.roomList.setRoomEncryption(roomId, config); } @@ -2754,7 +2743,7 @@ export class Crypto extends TypedEventEmitter { + public async encryptEvent(event: MatrixEvent, room?: Room): Promise { if (!room) { throw new Error("Cannot send encrypted messages in unknown rooms"); } @@ -2862,8 +2851,8 @@ export class Crypto extends TypedEventEmitter; - let alg: DecryptionAlgorithm; + let decryptors: Map | undefined; + let alg: DecryptionAlgorithm | undefined; roomId = roomId || null; if (roomId) { @@ -3799,10 +3788,10 @@ export class Crypto extends TypedEventEmitter>> { - if (typeof force === "number") { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - backwards compatibility - log = failedServers; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - backwards compatibility - failedServers = otkTimeout; - otkTimeout = force; - force = false; - } - const devicesWithoutSession: [string, string][] = [ // [userId, deviceId], ... ]; @@ -365,7 +353,7 @@ export async function ensureOlmSessionsForDevices( } const deviceRes = userRes[deviceId] || {}; - let oneTimeKey: IOneTimeKey = null; + let oneTimeKey: IOneTimeKey | null = null; for (const keyId in deviceRes) { if (keyId.indexOf(oneTimeKeyAlgorithm + ":") === 0) { oneTimeKey = deviceRes[keyId]; @@ -388,7 +376,7 @@ export async function ensureOlmSessionsForDevices( olmDevice, oneTimeKey, userId, deviceInfo, ).then((sid) => { if (resolveSession[key]) { - resolveSession[key](sid); + resolveSession[key](sid ?? undefined); } result[userId][deviceId].sessionId = sid; }, (e) => { @@ -413,7 +401,7 @@ async function _verifyKeyAndStartSession( oneTimeKey: IOneTimeKey, userId: string, deviceInfo: DeviceInfo, -): Promise { +): Promise { const deviceId = deviceInfo.deviceId; try { await verifySignature( diff --git a/src/crypto/store/base.ts b/src/crypto/store/base.ts index d266d3dca17..0615822271d 100644 --- a/src/crypto/store/base.ts +++ b/src/crypto/store/base.ts @@ -90,14 +90,14 @@ export interface CryptoStore { deviceKey: string, sessionId: string, txn: unknown, - func: (session: ISessionInfo) => void, + func: (session: ISessionInfo | null) => void, ): void; getEndToEndSessions( deviceKey: string, txn: unknown, func: (sessions: { [sessionId: string]: ISessionInfo }) => void, ): void; - getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void; + getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo | null) => void): void; storeEndToEndSession(deviceKey: string, sessionId: string, sessionInfo: ISessionInfo, txn: unknown): void; storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise; getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise; diff --git a/src/crypto/store/indexeddb-crypto-store-backend.ts b/src/crypto/store/indexeddb-crypto-store-backend.ts index 45de1d6045b..56784189796 100644 --- a/src/crypto/store/indexeddb-crypto-store-backend.ts +++ b/src/crypto/store/indexeddb-crypto-store-backend.ts @@ -307,7 +307,7 @@ export class Backend implements CryptoStore { expectedState: number, updates: Partial, ): Promise { - let result: OutgoingRoomKeyRequest = null; + let result: OutgoingRoomKeyRequest | null = null; function onsuccess(this: IDBRequest) { const cursor = this.result; @@ -375,7 +375,7 @@ export class Backend implements CryptoStore { try { func(getReq.result || null); } catch (e) { - abortWithException(txn, e); + abortWithException(txn, e); } }; } @@ -395,7 +395,7 @@ export class Backend implements CryptoStore { try { func(getReq.result || null); } catch (e) { - abortWithException(txn, e); + abortWithException(txn, e); } }; } @@ -411,7 +411,7 @@ export class Backend implements CryptoStore { try { func(getReq.result || null); } catch (e) { - abortWithException(txn, e); + abortWithException(txn, e); } }; } @@ -439,7 +439,7 @@ export class Backend implements CryptoStore { try { func(countReq.result); } catch (e) { - abortWithException(txn, e); + abortWithException(txn, e); } }; } @@ -465,7 +465,7 @@ export class Backend implements CryptoStore { try { func(results); } catch (e) { - abortWithException(txn, e); + abortWithException(txn, e); } } }; @@ -475,7 +475,7 @@ export class Backend implements CryptoStore { deviceKey: string, sessionId: string, txn: IDBTransaction, - func: (sessions: { [ sessionId: string ]: ISessionInfo }) => void, + func: (session: ISessionInfo | null) => void, ): void { const objectStore = txn.objectStore("sessions"); const getReq = objectStore.get([deviceKey, sessionId]); @@ -490,12 +490,12 @@ export class Backend implements CryptoStore { func(null); } } catch (e) { - abortWithException(txn, e); + abortWithException(txn, e); } }; } - public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo) => void): void { + public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo | null) => void): void { const objectStore = txn.objectStore("sessions"); const getReq = objectStore.openCursor(); getReq.onsuccess = function() { @@ -508,7 +508,7 @@ export class Backend implements CryptoStore { func(null); } } catch (e) { - abortWithException(txn, e); + abortWithException(txn, e); } }; } @@ -537,11 +537,11 @@ export class Backend implements CryptoStore { fixed, time: Date.now(), }); - return promiseifyTxn(txn); + await promiseifyTxn(txn); } public async getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise { - let result; + let result: IProblem | null = null; const txn = this.db.transaction("session_problems", "readwrite"); const objectStore = txn.objectStore("session_problems"); const index = objectStore.index("deviceKey"); @@ -604,8 +604,8 @@ export class Backend implements CryptoStore { txn: IDBTransaction, func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void, ): void { - let session: InboundGroupSessionData | boolean = false; - let withheld: IWithheld | boolean = false; + let session: InboundGroupSessionData | null | boolean = false; + let withheld: IWithheld | null | boolean = false; const objectStore = txn.objectStore("inbound_group_sessions"); const getReq = objectStore.get([senderCurve25519Key, sessionId]); getReq.onsuccess = function() { @@ -619,7 +619,7 @@ export class Backend implements CryptoStore { func(session as InboundGroupSessionData, withheld as IWithheld); } } catch (e) { - abortWithException(txn, e); + abortWithException(txn, e); } }; @@ -636,7 +636,7 @@ export class Backend implements CryptoStore { func(session as InboundGroupSessionData, withheld as IWithheld); } } catch (e) { - abortWithException(txn, e); + abortWithException(txn, e); } }; } @@ -654,14 +654,14 @@ export class Backend implements CryptoStore { sessionData: cursor.value.session, }); } catch (e) { - abortWithException(txn, e); + abortWithException(txn, e); } cursor.continue(); } else { try { func(null); } catch (e) { - abortWithException(txn, e); + abortWithException(txn, e); } } }; @@ -726,7 +726,7 @@ export class Backend implements CryptoStore { try { func(getReq.result || null); } catch (e) { - abortWithException(txn, e); + abortWithException(txn, e); } }; } @@ -754,7 +754,7 @@ export class Backend implements CryptoStore { try { func(rooms); } catch (e) { - abortWithException(txn, e); + abortWithException(txn, e); } } }; @@ -1050,7 +1050,7 @@ function abortWithException(txn: IDBTransaction, e: Error) { } } -function promiseifyTxn(txn: IDBTransaction): Promise { +function promiseifyTxn(txn: IDBTransaction): Promise { return new Promise((resolve, reject) => { txn.oncomplete = () => { if ((txn as IWrappedIDBTransaction)._mx_abortexception !== undefined) { diff --git a/src/crypto/store/indexeddb-crypto-store.ts b/src/crypto/store/indexeddb-crypto-store.ts index ad29510f917..4fbeafe19df 100644 --- a/src/crypto/store/indexeddb-crypto-store.ts +++ b/src/crypto/store/indexeddb-crypto-store.ts @@ -18,7 +18,7 @@ import { logger, PrefixedLogger } from '../../logger'; import { LocalStorageCryptoStore } from './localStorage-crypto-store'; import { MemoryCryptoStore } from './memory-crypto-store'; import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend'; -import { InvalidCryptoStoreError } from '../../errors'; +import { InvalidCryptoStoreError, InvalidCryptoStoreState } from '../../errors'; import * as IndexedDBHelpers from "../../indexeddb-helpers"; import { CryptoStore, @@ -64,8 +64,8 @@ export class IndexedDBCryptoStore implements CryptoStore { return IndexedDBHelpers.exists(indexedDB, dbName); } - private backendPromise: Promise = null; - private backend: CryptoStore = null; + private backendPromise?: Promise; + private backend?: CryptoStore; /** * Create a new IndexedDBCryptoStore @@ -141,7 +141,7 @@ export class IndexedDBCryptoStore implements CryptoStore { logger.warn("Crypto DB is too new for us to use!", e); // don't fall back to a different store: the user has crypto data // in this db so we should use it or nothing at all. - throw new InvalidCryptoStoreError(InvalidCryptoStoreError.TOO_NEW); + throw new InvalidCryptoStoreError(InvalidCryptoStoreState.TooNew); } logger.warn( `unable to connect to indexeddb ${this.dbName}` + @@ -213,7 +213,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * same instance as passed in, or the existing one. */ public getOrAddOutgoingRoomKeyRequest(request: OutgoingRoomKeyRequest): Promise { - return this.backend.getOrAddOutgoingRoomKeyRequest(request); + return this.backend!.getOrAddOutgoingRoomKeyRequest(request); } /** @@ -227,7 +227,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * not found */ public getOutgoingRoomKeyRequest(requestBody: IRoomKeyRequestBody): Promise { - return this.backend.getOutgoingRoomKeyRequest(requestBody); + return this.backend!.getOutgoingRoomKeyRequest(requestBody); } /** @@ -241,7 +241,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * requests in those states, an arbitrary one is chosen. */ public getOutgoingRoomKeyRequestByState(wantedStates: number[]): Promise { - return this.backend.getOutgoingRoomKeyRequestByState(wantedStates); + return this.backend!.getOutgoingRoomKeyRequestByState(wantedStates); } /** @@ -252,7 +252,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * @return {Promise>} Returns an array of requests in the given state */ public getAllOutgoingRoomKeyRequestsByState(wantedState: number): Promise { - return this.backend.getAllOutgoingRoomKeyRequestsByState(wantedState); + return this.backend!.getAllOutgoingRoomKeyRequestsByState(wantedState); } /** @@ -270,7 +270,7 @@ export class IndexedDBCryptoStore implements CryptoStore { deviceId: string, wantedStates: number[], ): Promise { - return this.backend.getOutgoingRoomKeyRequestsByTarget( + return this.backend!.getOutgoingRoomKeyRequestsByTarget( userId, deviceId, wantedStates, ); } @@ -292,7 +292,7 @@ export class IndexedDBCryptoStore implements CryptoStore { expectedState: number, updates: Partial, ): Promise { - return this.backend.updateOutgoingRoomKeyRequest( + return this.backend!.updateOutgoingRoomKeyRequest( requestId, expectedState, updates, ); } @@ -310,7 +310,7 @@ export class IndexedDBCryptoStore implements CryptoStore { requestId: string, expectedState: number, ): Promise { - return this.backend.deleteOutgoingRoomKeyRequest(requestId, expectedState); + return this.backend!.deleteOutgoingRoomKeyRequest(requestId, expectedState); } // Olm Account @@ -323,7 +323,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * @param {function(string)} func Called with the account pickle */ public getAccount(txn: IDBTransaction, func: (accountPickle: string | null) => void) { - this.backend.getAccount(txn, func); + this.backend!.getAccount(txn, func); } /** @@ -334,7 +334,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * @param {string} accountPickle The new account pickle to store. */ public storeAccount(txn: IDBTransaction, accountPickle: string): void { - this.backend.storeAccount(txn, accountPickle); + this.backend!.storeAccount(txn, accountPickle); } /** @@ -349,7 +349,7 @@ export class IndexedDBCryptoStore implements CryptoStore { txn: IDBTransaction, func: (keys: Record | null) => void, ): void { - this.backend.getCrossSigningKeys(txn, func); + this.backend!.getCrossSigningKeys(txn, func); } /** @@ -362,7 +362,7 @@ export class IndexedDBCryptoStore implements CryptoStore { func: (key: SecretStorePrivateKeys[K] | null) => void, type: K, ): void { - this.backend.getSecretStorePrivateKey(txn, func, type); + this.backend!.getSecretStorePrivateKey(txn, func, type); } /** @@ -372,7 +372,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * @param {string} keys keys object as getCrossSigningKeys() */ public storeCrossSigningKeys(txn: IDBTransaction, keys: Record): void { - this.backend.storeCrossSigningKeys(txn, keys); + this.backend!.storeCrossSigningKeys(txn, keys); } /** @@ -387,7 +387,7 @@ export class IndexedDBCryptoStore implements CryptoStore { type: K, key: SecretStorePrivateKeys[K], ): void { - this.backend.storeSecretStorePrivateKey(txn, type, key); + this.backend!.storeSecretStorePrivateKey(txn, type, key); } // Olm sessions @@ -398,7 +398,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * @param {function(int)} func Called with the count of sessions */ public countEndToEndSessions(txn: IDBTransaction, func: (count: number) => void): void { - this.backend.countEndToEndSessions(txn, func); + this.backend!.countEndToEndSessions(txn, func); } /** @@ -417,9 +417,9 @@ export class IndexedDBCryptoStore implements CryptoStore { deviceKey: string, sessionId: string, txn: IDBTransaction, - func: (sessions: { [ sessionId: string ]: ISessionInfo }) => void, + func: (session: ISessionInfo | null) => void, ): void { - this.backend.getEndToEndSession(deviceKey, sessionId, txn, func); + this.backend!.getEndToEndSession(deviceKey, sessionId, txn, func); } /** @@ -438,7 +438,7 @@ export class IndexedDBCryptoStore implements CryptoStore { txn: IDBTransaction, func: (sessions: { [sessionId: string]: ISessionInfo }) => void, ): void { - this.backend.getEndToEndSessions(deviceKey, txn, func); + this.backend!.getEndToEndSessions(deviceKey, txn, func); } /** @@ -448,8 +448,8 @@ export class IndexedDBCryptoStore implements CryptoStore { * an object with, deviceKey, lastReceivedMessageTs, sessionId * and session keys. */ - public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo) => void): void { - this.backend.getAllEndToEndSessions(txn, func); + public getAllEndToEndSessions(txn: IDBTransaction, func: (session: ISessionInfo | null) => void): void { + this.backend!.getAllEndToEndSessions(txn, func); } /** @@ -465,19 +465,19 @@ export class IndexedDBCryptoStore implements CryptoStore { sessionInfo: ISessionInfo, txn: IDBTransaction, ): void { - this.backend.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn); + this.backend!.storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn); } public storeEndToEndSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise { - return this.backend.storeEndToEndSessionProblem(deviceKey, type, fixed); + return this.backend!.storeEndToEndSessionProblem(deviceKey, type, fixed); } public getEndToEndSessionProblem(deviceKey: string, timestamp: number): Promise { - return this.backend.getEndToEndSessionProblem(deviceKey, timestamp); + return this.backend!.getEndToEndSessionProblem(deviceKey, timestamp); } public filterOutNotifiedErrorDevices(devices: IOlmDevice[]): Promise { - return this.backend.filterOutNotifiedErrorDevices(devices); + return this.backend!.filterOutNotifiedErrorDevices(devices); } // Inbound group sessions @@ -497,7 +497,7 @@ export class IndexedDBCryptoStore implements CryptoStore { txn: IDBTransaction, func: (groupSession: InboundGroupSessionData | null, groupSessionWithheld: IWithheld | null) => void, ): void { - this.backend.getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func); + this.backend!.getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func); } /** @@ -511,7 +511,7 @@ export class IndexedDBCryptoStore implements CryptoStore { txn: IDBTransaction, func: (session: ISession | null) => void, ): void { - this.backend.getAllEndToEndInboundGroupSessions(txn, func); + this.backend!.getAllEndToEndInboundGroupSessions(txn, func); } /** @@ -529,7 +529,7 @@ export class IndexedDBCryptoStore implements CryptoStore { sessionData: InboundGroupSessionData, txn: IDBTransaction, ): void { - this.backend.addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn); + this.backend!.addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn); } /** @@ -547,7 +547,7 @@ export class IndexedDBCryptoStore implements CryptoStore { sessionData: InboundGroupSessionData, txn: IDBTransaction, ): void { - this.backend.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn); + this.backend!.storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn); } public storeEndToEndInboundGroupSessionWithheld( @@ -556,7 +556,7 @@ export class IndexedDBCryptoStore implements CryptoStore { sessionData: IWithheld, txn: IDBTransaction, ): void { - this.backend.storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn); + this.backend!.storeEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId, sessionData, txn); } // End-to-end device tracking @@ -572,7 +572,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * @param {*} txn An active transaction. See doTxn(). */ public storeEndToEndDeviceData(deviceData: IDeviceData, txn: IDBTransaction): void { - this.backend.storeEndToEndDeviceData(deviceData, txn); + this.backend!.storeEndToEndDeviceData(deviceData, txn); } /** @@ -583,7 +583,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * device data */ public getEndToEndDeviceData(txn: IDBTransaction, func: (deviceData: IDeviceData | null) => void): void { - this.backend.getEndToEndDeviceData(txn, func); + this.backend!.getEndToEndDeviceData(txn, func); } // End to End Rooms @@ -595,7 +595,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * @param {*} txn An active transaction. See doTxn(). */ public storeEndToEndRoom(roomId: string, roomInfo: IRoomEncryption, txn: IDBTransaction): void { - this.backend.storeEndToEndRoom(roomId, roomInfo, txn); + this.backend!.storeEndToEndRoom(roomId, roomInfo, txn); } /** @@ -604,7 +604,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * @param {function(Object)} func Function called with the end to end encrypted rooms */ public getEndToEndRooms(txn: IDBTransaction, func: (rooms: Record) => void): void { - this.backend.getEndToEndRooms(txn, func); + this.backend!.getEndToEndRooms(txn, func); } // session backups @@ -616,7 +616,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * @returns {Promise} resolves to an array of inbound group sessions */ public getSessionsNeedingBackup(limit: number): Promise { - return this.backend.getSessionsNeedingBackup(limit); + return this.backend!.getSessionsNeedingBackup(limit); } /** @@ -625,7 +625,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * @returns {Promise} resolves to the number of sessions */ public countSessionsNeedingBackup(txn?: IDBTransaction): Promise { - return this.backend.countSessionsNeedingBackup(txn); + return this.backend!.countSessionsNeedingBackup(txn); } /** @@ -635,7 +635,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * @returns {Promise} resolves when the sessions are unmarked */ public unmarkSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise { - return this.backend.unmarkSessionsNeedingBackup(sessions, txn); + return this.backend!.unmarkSessionsNeedingBackup(sessions, txn); } /** @@ -645,7 +645,7 @@ export class IndexedDBCryptoStore implements CryptoStore { * @returns {Promise} resolves when the sessions are marked */ public markSessionsNeedingBackup(sessions: ISession[], txn?: IDBTransaction): Promise { - return this.backend.markSessionsNeedingBackup(sessions, txn); + return this.backend!.markSessionsNeedingBackup(sessions, txn); } /** @@ -661,7 +661,7 @@ export class IndexedDBCryptoStore implements CryptoStore { sessionId: string, txn?: IDBTransaction, ): void { - this.backend.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn); + this.backend!.addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn); } /** @@ -674,7 +674,7 @@ export class IndexedDBCryptoStore implements CryptoStore { roomId: string, txn?: IDBTransaction, ): Promise<[senderKey: string, sessionId: string][]> { - return this.backend.getSharedHistoryInboundGroupSessions(roomId, txn); + return this.backend!.getSharedHistoryInboundGroupSessions(roomId, txn); } /** @@ -685,7 +685,7 @@ export class IndexedDBCryptoStore implements CryptoStore { parkedData: ParkedSharedHistory, txn?: IDBTransaction, ): void { - this.backend.addParkedSharedHistory(roomId, parkedData, txn); + this.backend!.addParkedSharedHistory(roomId, parkedData, txn); } /** @@ -695,7 +695,7 @@ export class IndexedDBCryptoStore implements CryptoStore { roomId: string, txn?: IDBTransaction, ): Promise { - return this.backend.takeParkedSharedHistory(roomId, txn); + return this.backend!.takeParkedSharedHistory(roomId, txn); } /** @@ -720,7 +720,12 @@ export class IndexedDBCryptoStore implements CryptoStore { * reject with that exception. On synchronous backends, the * exception will propagate to the caller of the getFoo method. */ - doTxn(mode: Mode, stores: Iterable, func: (txn: IDBTransaction) => T, log?: PrefixedLogger): Promise { - return this.backend.doTxn(mode, stores, func, log); + public doTxn( + mode: Mode, + stores: Iterable, + func: (txn: IDBTransaction) => T, + log?: PrefixedLogger, + ): Promise { + return this.backend!.doTxn(mode, stores, func as (txn: unknown) => T, log); } } diff --git a/src/crypto/store/localStorage-crypto-store.ts b/src/crypto/store/localStorage-crypto-store.ts index 896dff8f156..4bc8b45ef7f 100644 --- a/src/crypto/store/localStorage-crypto-store.ts +++ b/src/crypto/store/localStorage-crypto-store.ts @@ -69,7 +69,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { public static exists(store: Storage): boolean { const length = store.length; for (let i = 0; i < length; i++) { - if (store.key(i).startsWith(E2E_PREFIX)) { + if (store.key(i)?.startsWith(E2E_PREFIX)) { return true; } } @@ -85,7 +85,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { public countEndToEndSessions(txn: unknown, func: (count: number) => void): void { let count = 0; for (let i = 0; i < this.store.length; ++i) { - if (this.store.key(i).startsWith(keyEndToEndSessions(''))) ++count; + if (this.store.key(i)?.startsWith(keyEndToEndSessions(''))) ++count; } func(count); } @@ -129,8 +129,8 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { public getAllEndToEndSessions(txn: unknown, func: (session: ISessionInfo) => void): void { for (let i = 0; i < this.store.length; ++i) { - if (this.store.key(i).startsWith(keyEndToEndSessions(''))) { - const deviceKey = this.store.key(i).split('/')[1]; + if (this.store.key(i)?.startsWith(keyEndToEndSessions(''))) { + const deviceKey = this.store.key(i)!.split('/')[1]; for (const sess of Object.values(this._getEndToEndSessions(deviceKey))) { func(sess); } @@ -220,7 +220,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { public getAllEndToEndInboundGroupSessions(txn: unknown, func: (session: ISession | null) => void): void { for (let i = 0; i < this.store.length; ++i) { const key = this.store.key(i); - if (key.startsWith(KEY_INBOUND_SESSION_PREFIX)) { + if (key?.startsWith(KEY_INBOUND_SESSION_PREFIX)) { // we can't use split, as the components we are trying to split out // might themselves contain '/' characters. We rely on the // senderKey being a (32-byte) curve25519 key, base64-encoded @@ -229,7 +229,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { func({ senderKey: key.slice(KEY_INBOUND_SESSION_PREFIX.length, KEY_INBOUND_SESSION_PREFIX.length + 43), sessionId: key.slice(KEY_INBOUND_SESSION_PREFIX.length + 44), - sessionData: getJsonItem(this.store, key), + sessionData: getJsonItem(this.store, key)!, }); } } @@ -297,9 +297,9 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { for (let i = 0; i < this.store.length; ++i) { const key = this.store.key(i); - if (key.startsWith(prefix)) { + if (key?.startsWith(prefix)) { const roomId = key.slice(prefix.length); - result[roomId] = getJsonItem(this.store, key); + result[roomId] = getJsonItem(this.store, key)!; } } func(result); @@ -320,7 +320,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { sessions.push({ senderKey: senderKey, sessionId: sessionId, - sessionData: sessionData, + sessionData: sessionData!, }); }, ); @@ -417,10 +417,10 @@ function getJsonItem(store: Storage, key: string): T | null { try { // if the key is absent, store.getItem() returns null, and // JSON.parse(null) === null, so this returns null. - return JSON.parse(store.getItem(key)); + return JSON.parse(store.getItem(key)!); } catch (e) { - logger.log("Error: Failed to get key %s: %s", key, e.stack || e); - logger.log(e.stack); + logger.log("Error: Failed to get key %s: %s", key, (e).message); + logger.log((e).stack); } return null; } diff --git a/src/crypto/store/memory-crypto-store.ts b/src/crypto/store/memory-crypto-store.ts index 8dc9a772e62..8b0206eef7a 100644 --- a/src/crypto/store/memory-crypto-store.ts +++ b/src/crypto/store/memory-crypto-store.ts @@ -54,7 +54,7 @@ export class MemoryCryptoStore implements CryptoStore { private inboundGroupSessions: { [sessionKey: string]: InboundGroupSessionData } = {}; private inboundGroupSessionsWithheld: Record = {}; // Opaque device data object - private deviceData: IDeviceData = null; + private deviceData: IDeviceData | null = null; private rooms: { [roomId: string]: IRoomEncryption } = {}; private sessionsNeedingBackup: { [sessionKey: string]: boolean } = {}; private sharedHistoryInboundGroupSessions: { [roomId: string]: [senderKey: string, sessionId: string][] } = {}; diff --git a/src/crypto/verification/Base.ts b/src/crypto/verification/Base.ts index 72cd71d9e8d..7c68eecf169 100644 --- a/src/crypto/verification/Base.ts +++ b/src/crypto/verification/Base.ts @@ -55,14 +55,14 @@ export class VerificationBase< > extends TypedEventEmitter { private cancelled = false; private _done = false; - private promise: Promise = null; - private transactionTimeoutTimer: ReturnType = null; - protected expectedEvent: string; - private resolve: () => void; - private reject: (e: Error | MatrixEvent) => void; - private resolveEvent: (e: MatrixEvent) => void; - private rejectEvent: (e: Error) => void; - private started: boolean; + private promise: Promise | null = null; + private transactionTimeoutTimer: ReturnType | null = null; + protected expectedEvent?: string; + private resolve?: () => void; + private reject?: (e: Error | MatrixEvent) => void; + private resolveEvent?: (e: MatrixEvent) => void; + private rejectEvent?: (e: Error) => void; + private started?: boolean; /** * Base class for verification methods. @@ -187,7 +187,7 @@ export class VerificationBase< this.expectedEvent = undefined; this.rejectEvent = undefined; this.resetTimer(); - this.resolveEvent(e); + this.resolveEvent?.(e); } } else if (e.getType() === EventType.KeyVerificationCancel) { const reject = this.reject; @@ -218,11 +218,11 @@ export class VerificationBase< } } - public done(): Promise { + public async done(): Promise { this.endTimer(); // always kill the activity timer if (!this._done) { this.request.onVerifierFinished(); - this.resolve(); + this.resolve?.(); return requestKeysDuringVerification(this.baseApis, this.userId, this.deviceId); } } @@ -291,7 +291,7 @@ export class VerificationBase< this.endTimer(); resolve(...args); }; - this.reject = (e: Error) => { + this.reject = (e: Error | MatrixEvent) => { this._done = true; this.endTimer(); reject(e); @@ -301,12 +301,12 @@ export class VerificationBase< this.started = true; this.resetTimer(); // restart the timeout new Promise((resolve, reject) => { - const crossSignId = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(this.userId)?.getId(); + const crossSignId = this.baseApis.crypto!.deviceList.getStoredCrossSigningForUser(this.userId)?.getId(); if (crossSignId === this.deviceId) { reject(new Error("Device ID is the same as the cross-signing ID")); } resolve(); - }).then(() => this.doVerification()).then(this.done.bind(this), this.cancel.bind(this)); + }).then(() => this.doVerification!()).then(this.done.bind(this), this.cancel.bind(this)); } return this.promise; } @@ -326,7 +326,7 @@ export class VerificationBase< verifier(keyId, device, keyInfo); verifiedDevices.push([deviceId, keyId, device.keys[keyId]]); } else { - const crossSigningInfo = this.baseApis.crypto.deviceList.getStoredCrossSigningForUser(userId); + const crossSigningInfo = this.baseApis.crypto!.deviceList.getStoredCrossSigningForUser(userId); if (crossSigningInfo && crossSigningInfo.getId() === deviceId) { verifier(keyId, DeviceInfo.fromStorage({ keys: { @@ -356,7 +356,7 @@ export class VerificationBase< // to upload each signature in a separate API call which is silly because the // API supports as many signatures as you like. for (const [deviceId, keyId, key] of verifiedDevices) { - await this.baseApis.crypto.setDeviceVerification(userId, deviceId, true, null, null, { [keyId]: key }); + await this.baseApis.crypto!.setDeviceVerification(userId, deviceId, true, null, null, { [keyId]: key }); } // if one of the user's own devices is being marked as verified / unverified, diff --git a/src/crypto/verification/QRCode.ts b/src/crypto/verification/QRCode.ts index 3c16c4955c9..8bb3ca0d9bf 100644 --- a/src/crypto/verification/QRCode.ts +++ b/src/crypto/verification/QRCode.ts @@ -49,7 +49,7 @@ type EventHandlerMap = { * @extends {module:crypto/verification/Base} */ export class ReciprocateQRCode extends Base { - public reciprocateQREvent: IReciprocateQr; + public reciprocateQREvent?: IReciprocateQr; public static factory( channel: IVerificationChannel, @@ -76,7 +76,7 @@ export class ReciprocateQRCode extends Base { const { qrCodeData } = this.request; // 1. check the secret - if (this.startEvent.getContent()['secret'] !== qrCodeData.encodedSharedSecret) { + if (this.startEvent.getContent()['secret'] !== qrCodeData?.encodedSharedSecret) { throw newKeyMismatchError(); } @@ -92,21 +92,21 @@ export class ReciprocateQRCode extends Base { // 3. determine key to sign / mark as trusted const keys: Record = {}; - switch (qrCodeData.mode) { + switch (qrCodeData?.mode) { case Mode.VerifyOtherUser: { // add master key to keys to be signed, only if we're not doing self-verification const masterKey = qrCodeData.otherUserMasterKey; - keys[`ed25519:${masterKey}`] = masterKey; + keys[`ed25519:${masterKey}`] = masterKey!; break; } case Mode.VerifySelfTrusted: { const deviceId = this.request.targetDevice.deviceId; - keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey; + keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey!; break; } case Mode.VerifySelfUntrusted: { const masterKey = qrCodeData.myMasterKey; - keys[`ed25519:${masterKey}`] = masterKey; + keys[`ed25519:${masterKey}`] = masterKey!; break; } } @@ -158,41 +158,41 @@ export class QRCodeData { public readonly mode: Mode, private readonly sharedSecret: string, // only set when mode is MODE_VERIFY_OTHER_USER, master key of other party at time of generating QR code - public readonly otherUserMasterKey: string | undefined, + public readonly otherUserMasterKey: string | null, // only set when mode is MODE_VERIFY_SELF_TRUSTED, device key of other party at time of generating QR code - public readonly otherDeviceKey: string | undefined, + public readonly otherDeviceKey: string | null, // only set when mode is MODE_VERIFY_SELF_UNTRUSTED, own master key at time of generating QR code - public readonly myMasterKey: string | undefined, + public readonly myMasterKey: string | null, private readonly buffer: Buffer, ) {} public static async create(request: VerificationRequest, client: MatrixClient): Promise { const sharedSecret = QRCodeData.generateSharedSecret(); const mode = QRCodeData.determineMode(request, client); - let otherUserMasterKey = null; - let otherDeviceKey = null; - let myMasterKey = null; + let otherUserMasterKey: string | null = null; + let otherDeviceKey: string | null = null; + let myMasterKey: string | null = null; if (mode === Mode.VerifyOtherUser) { - const otherUserCrossSigningInfo = - client.getStoredCrossSigningForUser(request.otherUserId); - otherUserMasterKey = otherUserCrossSigningInfo.getId("master"); + const otherUserCrossSigningInfo = client.getStoredCrossSigningForUser(request.otherUserId); + otherUserMasterKey = otherUserCrossSigningInfo!.getId("master"); } else if (mode === Mode.VerifySelfTrusted) { otherDeviceKey = await QRCodeData.getOtherDeviceKey(request, client); } else if (mode === Mode.VerifySelfUntrusted) { - const myUserId = client.getUserId(); + const myUserId = client.getUserId()!; const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId); - myMasterKey = myCrossSigningInfo.getId("master"); + myMasterKey = myCrossSigningInfo!.getId("master"); } const qrData = QRCodeData.generateQrData( - request, client, mode, + request, + client, + mode, sharedSecret, - otherUserMasterKey, - otherDeviceKey, - myMasterKey, + otherUserMasterKey!, + otherDeviceKey!, + myMasterKey!, ); const buffer = QRCodeData.generateBuffer(qrData); - return new QRCodeData(mode, sharedSecret, - otherUserMasterKey, otherDeviceKey, myMasterKey, buffer); + return new QRCodeData(mode, sharedSecret, otherUserMasterKey, otherDeviceKey, myMasterKey, buffer); } /** @@ -213,12 +213,11 @@ export class QRCodeData { } private static async getOtherDeviceKey(request: VerificationRequest, client: MatrixClient): Promise { - const myUserId = client.getUserId(); + const myUserId = client.getUserId()!; const otherDevice = request.targetDevice; - const otherDeviceId = otherDevice ? otherDevice.deviceId : null; - const device = client.getStoredDevice(myUserId, otherDeviceId); + const device = otherDevice.deviceId ? client.getStoredDevice(myUserId, otherDevice.deviceId) : undefined; if (!device) { - throw new Error("could not find device " + otherDeviceId); + throw new Error("could not find device " + otherDevice?.deviceId); } return device.getFingerprint(); } @@ -245,11 +244,11 @@ export class QRCodeData { client: MatrixClient, mode: Mode, encodedSharedSecret: string, - otherUserMasterKey: string, - otherDeviceKey: string, - myMasterKey: string, + otherUserMasterKey?: string, + otherDeviceKey?: string, + myMasterKey?: string, ): IQrData { - const myUserId = client.getUserId(); + const myUserId = client.getUserId()!; const transactionId = request.channel.transactionId; const qrData = { prefix: BINARY_PREFIX, @@ -265,18 +264,18 @@ export class QRCodeData { if (mode === Mode.VerifyOtherUser) { // First key is our master cross signing key - qrData.firstKeyB64 = myCrossSigningInfo.getId("master"); + qrData.firstKeyB64 = myCrossSigningInfo!.getId("master")!; // Second key is the other user's master cross signing key - qrData.secondKeyB64 = otherUserMasterKey; + qrData.secondKeyB64 = otherUserMasterKey!; } else if (mode === Mode.VerifySelfTrusted) { // First key is our master cross signing key - qrData.firstKeyB64 = myCrossSigningInfo.getId("master"); - qrData.secondKeyB64 = otherDeviceKey; + qrData.firstKeyB64 = myCrossSigningInfo!.getId("master")!; + qrData.secondKeyB64 = otherDeviceKey!; } else if (mode === Mode.VerifySelfUntrusted) { // First key is our device's key - qrData.firstKeyB64 = client.getDeviceEd25519Key(); + qrData.firstKeyB64 = client.getDeviceEd25519Key()!; // Second key is what we think our master cross signing key is - qrData.secondKeyB64 = myMasterKey; + qrData.secondKeyB64 = myMasterKey!; } return qrData; } diff --git a/src/crypto/verification/request/VerificationRequest.ts b/src/crypto/verification/request/VerificationRequest.ts index 6ecba447eb2..76bf6980231 100644 --- a/src/crypto/verification/request/VerificationRequest.ts +++ b/src/crypto/verification/request/VerificationRequest.ts @@ -96,25 +96,25 @@ export class VerificationRequest< private eventsByUs = new Map(); private eventsByThem = new Map(); private _observeOnly = false; - private timeoutTimer: ReturnType = null; + private timeoutTimer: ReturnType | null = null; private _accepting = false; private _declining = false; private verifierHasFinished = false; private _cancelled = false; - private _chosenMethod: VerificationMethod = null; + private _chosenMethod: VerificationMethod | null = null; // we keep a copy of the QR Code data (including other user master key) around // for QR reciprocate verification, to protect against // cross-signing identity reset between the .ready and .start event // and signing the wrong key after .start - private _qrCodeData: QRCodeData = null; + private _qrCodeData: QRCodeData | null = null; // The timestamp when we received the request event from the other side - private requestReceivedAt: number = null; + private requestReceivedAt: number | null = null; private commonMethods: VerificationMethod[] = []; private _phase: Phase; public _cancellingUserId: string; // Used in tests only - private _verifier: VerificationBase; + private _verifier?: VerificationBase; constructor( public readonly channel: C, @@ -204,7 +204,7 @@ export class VerificationRequest< } /** the method picked in the .start event */ - public get chosenMethod(): VerificationMethod { + public get chosenMethod(): VerificationMethod | null { return this._chosenMethod; } @@ -236,7 +236,7 @@ export class VerificationRequest< * The key verification request event. * @returns {MatrixEvent} The request event, or falsey if not found. */ - public get requestEvent(): MatrixEvent { + public get requestEvent(): MatrixEvent | undefined { return this.getEventByEither(REQUEST_TYPE); } @@ -246,7 +246,7 @@ export class VerificationRequest< } /** The verifier to do the actual verification, once the method has been established. Only defined when the `phase` is PHASE_STARTED. */ - public get verifier(): VerificationBase { + public get verifier(): VerificationBase | undefined { return this._verifier; } @@ -270,7 +270,7 @@ export class VerificationRequest< } /** Only set after a .ready if the other party can scan a QR code */ - public get qrCodeData(): QRCodeData { + public get qrCodeData(): QRCodeData | null { return this._qrCodeData; } @@ -340,7 +340,7 @@ export class VerificationRequest< /** The id of the user that initiated the request */ public get requestingUserId(): string { if (this.initiatedByMe) { - return this.client.getUserId(); + return this.client.getUserId()!; } else { return this.otherUserId; } @@ -351,7 +351,7 @@ export class VerificationRequest< if (this.initiatedByMe) { return this.otherUserId; } else { - return this.client.getUserId(); + return this.client.getUserId()!; } } @@ -368,7 +368,7 @@ export class VerificationRequest< * The id of the user that cancelled the request, * only defined when phase is PHASE_CANCELLED */ - public get cancellingUserId(): string { + public get cancellingUserId(): string | undefined { const myCancel = this.eventsByUs.get(CANCEL_TYPE); const theirCancel = this.eventsByThem.get(CANCEL_TYPE); @@ -422,7 +422,7 @@ export class VerificationRequest< */ public beginKeyVerification( method: VerificationMethod, - targetDevice: ITargetDevice = null, + targetDevice: ITargetDevice | null = null, ): VerificationBase { // need to allow also when unsent in case of to_device if (!this.observeOnly && !this._verifier) { @@ -443,7 +443,7 @@ export class VerificationRequest< this._chosenMethod = method; } } - return this._verifier; + return this._verifier!; } /** @@ -470,7 +470,7 @@ export class VerificationRequest< if (this._verifier) { return this._verifier.cancel(errorFactory(code, reason)()); } else { - this._cancellingUserId = this.client.getUserId(); + this._cancellingUserId = this.client.getUserId()!; await this.channel.send(CANCEL_TYPE, { code, reason }); } } @@ -525,11 +525,11 @@ export class VerificationRequest< } } - private getEventByEither(type: string): MatrixEvent { + private getEventByEither(type: string): MatrixEvent | undefined { return this.eventsByThem.get(type) || this.eventsByUs.get(type); } - private getEventBy(type: string, byThem = false): MatrixEvent { + private getEventBy(type: string, byThem = false): MatrixEvent | undefined { if (byThem) { return this.eventsByThem.get(type); } else { @@ -548,20 +548,18 @@ export class VerificationRequest< transitions.push({ phase: PHASE_REQUESTED, event: requestEvent }); } - const readyEvent = - requestEvent && this.getEventBy(READY_TYPE, !hasRequestByThem); + const readyEvent = requestEvent && this.getEventBy(READY_TYPE, !hasRequestByThem); if (readyEvent && phase() === PHASE_REQUESTED) { transitions.push({ phase: PHASE_READY, event: readyEvent }); } - let startEvent; + let startEvent: MatrixEvent | undefined; if (readyEvent || !requestEvent) { const theirStartEvent = this.eventsByThem.get(START_TYPE); const ourStartEvent = this.eventsByUs.get(START_TYPE); // any party can send .start after a .ready or unsent if (theirStartEvent && ourStartEvent) { - startEvent = theirStartEvent.getSender() < ourStartEvent.getSender() ? - theirStartEvent : ourStartEvent; + startEvent = theirStartEvent.getSender() < ourStartEvent.getSender() ? theirStartEvent : ourStartEvent; } else { startEvent = theirStartEvent ? theirStartEvent : ourStartEvent; } @@ -569,7 +567,9 @@ export class VerificationRequest< startEvent = this.getEventBy(START_TYPE, !hasRequestByThem); } if (startEvent) { - const fromRequestPhase = phase() === PHASE_REQUESTED && requestEvent.getSender() !== startEvent.getSender(); + const fromRequestPhase = ( + phase() === PHASE_REQUESTED && requestEvent?.getSender() !== startEvent.getSender() + ); const fromUnsentPhase = phase() === PHASE_UNSENT && this.channel.canCreateRequest(START_TYPE); if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) { transitions.push({ phase: PHASE_STARTED, event: startEvent }); @@ -651,7 +651,7 @@ export class VerificationRequest< if (newEvent.getType() !== START_TYPE) { return false; } - const oldEvent = this._verifier.startEvent; + const oldEvent = this._verifier!.startEvent; let oldRaceIdentifier; if (this.isSelfVerification) { @@ -890,9 +890,9 @@ export class VerificationRequest< private createVerifier( method: VerificationMethod, - startEvent: MatrixEvent = null, - targetDevice: ITargetDevice = null, - ): VerificationBase { + startEvent: MatrixEvent | null = null, + targetDevice: ITargetDevice | null = null, + ): VerificationBase | undefined { if (!targetDevice) { targetDevice = this.targetDevice; } @@ -941,7 +941,7 @@ export class VerificationRequest< } } - public getEventFromOtherParty(type: string): MatrixEvent { + public getEventFromOtherParty(type: string): MatrixEvent | undefined { return this.eventsByThem.get(type); } } diff --git a/src/errors.js b/src/errors.js deleted file mode 100644 index 186fc69115f..00000000000 --- a/src/errors.js +++ /dev/null @@ -1,52 +0,0 @@ -// can't just do InvalidStoreError extends Error -// because of http://babeljs.io/docs/usage/caveats/#classes -export function InvalidStoreError(reason, value) { - const message = `Store is invalid because ${reason}, ` + - `please stop the client, delete all data and start the client again`; - 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); - -export function InvalidCryptoStoreError(reason) { - const message = `Crypto store is invalid because ${reason}, ` + - `please stop the client, delete all data and start the client again`; - const instance = Reflect.construct(Error, [message]); - Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this)); - instance.reason = reason; - instance.name = 'InvalidCryptoStoreError'; - return instance; -} - -InvalidCryptoStoreError.TOO_NEW = "TOO_NEW"; - -InvalidCryptoStoreError.prototype = Object.create(Error.prototype, { - constructor: { - value: Error, - enumerable: false, - writable: true, - configurable: true, - }, -}); -Reflect.setPrototypeOf(InvalidCryptoStoreError, Error); - -export class KeySignatureUploadError extends Error { - constructor(message, value) { - super(message); - this.value = value; - } -} diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 00000000000..f58c839ce60 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,51 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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. +*/ + +export enum InvalidStoreState { + ToggledLazyLoading, +} + +export class InvalidStoreError extends Error { + public static TOGGLED_LAZY_LOADING = InvalidStoreState.ToggledLazyLoading; + + public constructor(public readonly reason: InvalidStoreState, public readonly value: any) { + const message = `Store is invalid because ${reason}, ` + + `please stop the client, delete all data and start the client again`; + super(message); + this.name = "InvalidStoreError"; + } +} + +export enum InvalidCryptoStoreState { + TooNew = "TOO_NEW", +} + +export class InvalidCryptoStoreError extends Error { + public static TOO_NEW = InvalidCryptoStoreState.TooNew; + + public constructor(public readonly reason: InvalidCryptoStoreState) { + const message = `Crypto store is invalid because ${reason}, ` + + `please stop the client, delete all data and start the client again`; + super(message); + this.name = 'InvalidCryptoStoreError'; + } +} + +export class KeySignatureUploadError extends Error { + public constructor(message: string, public readonly value: any) { + super(message); + } +} diff --git a/src/event-mapper.ts b/src/event-mapper.ts index 38ee4267e66..57d36c9f68d 100644 --- a/src/event-mapper.ts +++ b/src/event-mapper.ts @@ -36,11 +36,11 @@ export function eventMapperFor(client: MatrixClient, options: MapperOpts): Event const room = client.getRoom(plainOldJsObject.room_id); - let event: MatrixEvent; + let event: MatrixEvent | undefined; // If the event is already known to the room, let's re-use the model rather than duplicating. // We avoid doing this to state events as they may be forward or backwards looking which tweaks behaviour. if (room && plainOldJsObject.state_key === undefined) { - event = room.findEventById(plainOldJsObject.event_id); + event = room.findEventById(plainOldJsObject.event_id!); } if (!event || event.status) { diff --git a/src/http-api/errors.ts b/src/http-api/errors.ts index 1610a86234e..faa379784f4 100644 --- a/src/http-api/errors.ts +++ b/src/http-api/errors.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { IUsageLimit } from "../@types/partials"; +import { MatrixEvent } from "../models/event"; interface IErrorJson extends Partial { [key: string]: any; // extensible @@ -50,7 +51,12 @@ export class MatrixError extends HTTPError { public readonly errcode?: string; public readonly data: IErrorJson; - constructor(errorJson: IErrorJson = {}, public readonly httpStatus?: number, public url?: string) { + constructor( + errorJson: IErrorJson = {}, + public readonly httpStatus?: number, + public url?: string, + public event?: MatrixEvent, + ) { let message = errorJson.error || "Unknown message"; if (httpStatus) { message = `[${httpStatus}] ${message}`; diff --git a/src/http-api/fetch.ts b/src/http-api/fetch.ts index 4fecaaecf8d..bce1fbd96e3 100644 --- a/src/http-api/fetch.ts +++ b/src/http-api/fetch.ts @@ -73,7 +73,7 @@ export class FetchHttpApi { public idServerRequest( method: Method, path: string, - params: Record, + params: Record | undefined, prefix: string, accessToken?: string, ): Promise> { @@ -96,7 +96,7 @@ export class FetchHttpApi { headers: {}, }; if (accessToken) { - opts.headers.Authorization = `Bearer ${accessToken}`; + opts.headers!.Authorization = `Bearer ${accessToken}`; } return this.requestOtherUrl(method, fullUri, body, opts); @@ -286,10 +286,10 @@ export class FetchHttpApi { credentials: "omit", // we send credentials via headers }); } catch (e) { - if (e.name === "AbortError") { + if ((e).name === "AbortError") { throw e; } - throw new ConnectionError("fetch failed", e); + throw new ConnectionError("fetch failed", e); } finally { cleanup(); } diff --git a/src/http-api/utils.ts b/src/http-api/utils.ts index 21ddfb9dd51..c7e39477089 100644 --- a/src/http-api/utils.ts +++ b/src/http-api/utils.ts @@ -72,11 +72,11 @@ export function anySignal(signals: AbortSignal[]): { * @returns {Error} */ export function parseErrorResponse(response: XMLHttpRequest | Response, body?: string): Error { - let contentType: ParsedMediaType; + let contentType: ParsedMediaType | null; try { contentType = getResponseContentType(response); } catch (e) { - return e; + return e; } if (contentType?.type === "application/json" && body) { diff --git a/src/logger.ts b/src/logger.ts index 6084314a3eb..9723d6abb6b 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -59,16 +59,16 @@ log.methodFactory = function(methodName, logLevel, loggerName) { * Drop-in replacement for console using {@link https://www.npmjs.com/package/loglevel|loglevel}. * Can be tailored down to specific use cases if needed. */ -export const logger: PrefixedLogger = log.getLogger(DEFAULT_NAMESPACE); +export const logger = log.getLogger(DEFAULT_NAMESPACE) as PrefixedLogger; logger.setLevel(log.levels.DEBUG, false); export interface PrefixedLogger extends Logger { - withPrefix?: (prefix: string) => PrefixedLogger; - prefix?: string; + withPrefix: (prefix: string) => PrefixedLogger; + prefix: string; } -function extendLogger(logger: PrefixedLogger) { - logger.withPrefix = function(prefix: string): PrefixedLogger { +function extendLogger(logger: Logger) { + (logger).withPrefix = function(prefix: string): PrefixedLogger { const existingPrefix = this.prefix || ""; return getPrefixedLogger(existingPrefix + prefix); }; @@ -77,7 +77,7 @@ function extendLogger(logger: PrefixedLogger) { extendLogger(logger); function getPrefixedLogger(prefix: string): PrefixedLogger { - const prefixLogger: PrefixedLogger = log.getLogger(`${DEFAULT_NAMESPACE}-${prefix}`); + const prefixLogger = log.getLogger(`${DEFAULT_NAMESPACE}-${prefix}`) as PrefixedLogger; if (prefixLogger.prefix !== prefix) { // Only do this setup work the first time through, as loggers are saved by name. extendLogger(prefixLogger); diff --git a/src/matrix.ts b/src/matrix.ts index eaa9b09e76b..d1999ed85c3 100644 --- a/src/matrix.ts +++ b/src/matrix.ts @@ -68,7 +68,7 @@ export function setCryptoStoreFactory(fac) { } export interface ICryptoCallbacks { - getCrossSigningKey?: (keyType: string, pubKey: string) => Promise; + getCrossSigningKey?: (keyType: string, pubKey: string) => Promise; saveCrossSigningKeys?: (keys: Record) => void; shouldUpgradeDeviceVerifications?: ( users: Record diff --git a/src/models/MSC3089TreeSpace.ts b/src/models/MSC3089TreeSpace.ts index 9a9deec68ac..762ef586ab6 100644 --- a/src/models/MSC3089TreeSpace.ts +++ b/src/models/MSC3089TreeSpace.ts @@ -86,7 +86,7 @@ export class MSC3089TreeSpace { public readonly room: Room; public constructor(private client: MatrixClient, public readonly roomId: string) { - this.room = this.client.getRoom(this.roomId); + this.room = this.client.getRoom(this.roomId)!; if (!this.room) throw new Error("Unknown room"); } @@ -282,7 +282,7 @@ export class MSC3089TreeSpace { const members = this.room.currentState.getStateEvents(EventType.RoomMember); for (const member of members) { const isNotUs = member.getStateKey() !== this.client.getUserId(); - if (isNotUs && kickMemberships.includes(member.getContent().membership)) { + if (isNotUs && kickMemberships.includes(member.getContent().membership!)) { const stateKey = member.getStateKey(); if (!stateKey) { throw new Error("State key not found for branch"); diff --git a/src/models/beacon.ts b/src/models/beacon.ts index 92e95079778..584cae6491a 100644 --- a/src/models/beacon.ts +++ b/src/models/beacon.ts @@ -51,21 +51,21 @@ export const getBeaconInfoIdentifier = (event: MatrixEvent): BeaconIdentifier => // https://github.com/matrix-org/matrix-spec-proposals/pull/3672 export class Beacon extends TypedEventEmitter, BeaconEventHandlerMap> { public readonly roomId: string; - private _beaconInfo: BeaconInfoState; - private _isLive: boolean; - private livenessWatchTimeout: ReturnType; - private _latestLocationEvent: MatrixEvent | undefined; + private _beaconInfo?: BeaconInfoState; + private _isLive?: boolean; + private livenessWatchTimeout?: ReturnType; + private _latestLocationEvent?: MatrixEvent; constructor( private rootEvent: MatrixEvent, ) { super(); this.setBeaconInfo(this.rootEvent); - this.roomId = this.rootEvent.getRoomId(); + this.roomId = this.rootEvent.getRoomId()!; } public get isLive(): boolean { - return this._isLive; + return !!this._isLive; } public get identifier(): BeaconIdentifier { @@ -77,14 +77,14 @@ export class Beacon extends TypedEventEmitter 1) { this.livenessWatchTimeout = setTimeout( () => { this.monitorLiveness(); }, expiryInMs, ); } - } else if (this._beaconInfo?.timestamp > Date.now()) { + } else if (this.beaconInfo.timestamp > Date.now()) { // beacon start timestamp is in the future // check liveness again then this.livenessWatchTimeout = setTimeout( () => { this.monitorLiveness(); }, - this.beaconInfo?.timestamp - Date.now(), + this.beaconInfo.timestamp - Date.now(), ); } } @@ -165,22 +166,22 @@ export class Beacon extends TypedEventEmitter this.latestLocationState.timestamp) + (!this.latestLocationState || timestamp > this.latestLocationState.timestamp!) ); }); const latestLocationEvent = validLocationEvents.sort(sortEventsByLatestContentTimestamp)?.[0]; if (latestLocationEvent) { this._latestLocationEvent = latestLocationEvent; - this.emit(BeaconEvent.LocationUpdate, this.latestLocationState); + this.emit(BeaconEvent.LocationUpdate, this.latestLocationState!); } } private clearLatestLocation = () => { this._latestLocationEvent = undefined; - this.emit(BeaconEvent.LocationUpdate, this.latestLocationState); + this.emit(BeaconEvent.LocationUpdate, this.latestLocationState!); }; private setBeaconInfo(event: MatrixEvent): void { @@ -195,9 +196,10 @@ export class Beacon extends TypedEventEmitter Date.now() ? - this._beaconInfo?.timestamp - 360000 /* 6min */ : - this._beaconInfo?.timestamp; + if (!this.beaconInfo) return; + const startTimestamp = this.beaconInfo.timestamp > Date.now() ? + this.beaconInfo.timestamp - 360000 /* 6min */ : + this.beaconInfo.timestamp; this._isLive = !!this._beaconInfo?.live && isTimestampInDuration(startTimestamp, this._beaconInfo?.timeout, Date.now()); diff --git a/src/models/event-timeline-set.ts b/src/models/event-timeline-set.ts index 83a96b5b449..d0033c6f699 100644 --- a/src/models/event-timeline-set.ts +++ b/src/models/event-timeline-set.ts @@ -822,7 +822,7 @@ export class EventTimelineSet extends TypedEventEmitter | null> = { [Direction.Backward]: null, [Direction.Forward]: null, @@ -131,9 +131,6 @@ export class EventTimeline { this.endState = new RoomState(this.roomId); this.endState.paginationToken = null; - this.prevTimeline = null; - this.nextTimeline = null; - // this is used by client.js this.paginationRequests = { 'b': null, 'f': null }; @@ -226,7 +223,7 @@ export class EventTimeline { * Get the ID of the room for this timeline * @return {string} room ID */ - public getRoomId(): string { + public getRoomId(): string | null { return this.roomId; } @@ -234,7 +231,7 @@ export class EventTimeline { * Get the filter for this timeline's timelineSet (if any) * @return {Filter} filter */ - public getFilter(): Filter { + public getFilter(): Filter | undefined { return this.eventTimelineSet.getFilter(); } @@ -324,7 +321,7 @@ export class EventTimeline { * @return {?EventTimeline} previous or following timeline, if they have been * joined up. */ - public getNeighbouringTimeline(direction: Direction): EventTimeline { + public getNeighbouringTimeline(direction: Direction): EventTimeline | null { if (direction == EventTimeline.BACKWARDS) { return this.prevTimeline; } else if (direction == EventTimeline.FORWARDS) { @@ -391,7 +388,7 @@ export class EventTimeline { roomState?: RoomState, ): void { let toStartOfTimeline = !!toStartOfTimelineOrOpts; - let timelineWasEmpty: boolean; + let timelineWasEmpty: boolean | undefined; if (typeof (toStartOfTimelineOrOpts) === 'object') { ({ toStartOfTimeline, roomState, timelineWasEmpty } = toStartOfTimelineOrOpts); } else if (toStartOfTimelineOrOpts !== undefined) { diff --git a/src/models/event.ts b/src/models/event.ts index 50de12973ac..559ec4c8730 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -34,6 +34,7 @@ import { TypedReEmitter } from '../ReEmitter'; import { MatrixError } from "../http-api"; import { TypedEventEmitter } from "./typed-event-emitter"; import { EventStatus } from "./event-status"; +import { DecryptionError } from "../crypto/algorithms"; export { EventStatus } from "./event-status"; @@ -685,14 +686,6 @@ export class MatrixEvent extends TypedEventEmitter { - // 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"); @@ -822,15 +815,19 @@ export class MatrixEvent extends TypedEventEmittere).detailedString}), but retrying`, e); continue; } // decryption error, no retries queued. Warn about the error and // set it to m.bad.encrypted. - logger.warn(`Got error decrypting event (id=${this.getId()}: ${e.detailedString})`, e); + logger.warn( + `Got error decrypting event (id=${this.getId()}: ${(e).detailedString})`, + e, + ); - res = this.badEncryptedMessage(e.message); + res = this.badEncryptedMessage((e).message); } // at this point, we've either successfully decrypted the event, or have given up diff --git a/src/models/relations-container.ts b/src/models/relations-container.ts index 9b2f255eea0..e08b80cbdd0 100644 --- a/src/models/relations-container.ts +++ b/src/models/relations-container.ts @@ -118,31 +118,31 @@ export class RelationsContainer { const { event_id: relatesToEventId, rel_type: relationType } = relation; const eventType = event.getType(); - let relationsForEvent = this.relations.get(relatesToEventId); + let relationsForEvent = this.relations.get(relatesToEventId!); if (!relationsForEvent) { relationsForEvent = new Map>(); - this.relations.set(relatesToEventId, relationsForEvent); + this.relations.set(relatesToEventId!, relationsForEvent); } - let relationsWithRelType = relationsForEvent.get(relationType); + let relationsWithRelType = relationsForEvent.get(relationType!); if (!relationsWithRelType) { relationsWithRelType = new Map(); - relationsForEvent.set(relationType, relationsWithRelType); + relationsForEvent.set(relationType!, relationsWithRelType); } let relationsWithEventType = relationsWithRelType.get(eventType); if (!relationsWithEventType) { relationsWithEventType = new Relations( - relationType, + relationType!, eventType, this.client, ); relationsWithRelType.set(eventType, relationsWithEventType); const room = this.room ?? timelineSet?.room; - const relatesToEvent = timelineSet?.findEventById(relatesToEventId) - ?? room?.findEventById(relatesToEventId) - ?? room?.getPendingEvent(relatesToEventId); + const relatesToEvent = timelineSet?.findEventById(relatesToEventId!) + ?? room?.findEventById(relatesToEventId!) + ?? room?.getPendingEvent(relatesToEventId!); if (relatesToEvent) { relationsWithEventType.setTargetEvent(relatesToEvent); } diff --git a/src/models/relations.ts b/src/models/relations.ts index 526a00bf57a..070e5e46050 100644 --- a/src/models/relations.ts +++ b/src/models/relations.ts @@ -47,7 +47,7 @@ export class Relations extends TypedEventEmitter> = {}; private annotationsBySender: Record> = {}; private sortedAnnotationsByKey: [string, Set][] = []; - private targetEvent: MatrixEvent = null; + private targetEvent: MatrixEvent | null = null; private creationEmitted = false; private readonly client: MatrixClient; @@ -107,7 +107,7 @@ export class Relations extends TypedEventEmitter][] | null { if (this.relationType !== RelationType.Annotation) { // Other relation types are not grouped currently. return null; @@ -301,7 +297,7 @@ export class Relations extends TypedEventEmitter> | null { if (this.relationType !== RelationType.Annotation) { // Other relation types are not grouped currently. return null; @@ -335,8 +331,8 @@ export class Relations extends TypedEventEmitter(RelationType.Replace); const minTs = replaceRelation?.origin_server_ts; - const lastReplacement = this.getRelations().reduce((last, event) => { - if (event.getSender() !== this.targetEvent.getSender()) { + const lastReplacement = this.getRelations().reduce((last, event) => { + if (event.getSender() !== this.targetEvent!.getSender()) { return last; } if (minTs && minTs > event.getTs()) { @@ -348,8 +344,8 @@ export class Relations extends TypedEventEmitter { // find the earliest unfiltered timeline let timeline = unfilteredLiveTimeline; while (timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)) { - timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS); + timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)!; } timelineSet.getLiveTimeline().setPaginationToken( diff --git a/src/models/thread.ts b/src/models/thread.ts index dc433a24fa8..e77e3f5b830 100644 --- a/src/models/thread.ts +++ b/src/models/thread.ts @@ -83,7 +83,7 @@ export class Thread extends ReadReceipt { private reEmitter: TypedReEmitter; - private lastEvent: MatrixEvent; + private lastEvent!: MatrixEvent; private replyCount = 0; public readonly room: Room; @@ -185,7 +185,7 @@ export class Thread extends ReadReceipt { this.lastEvent = events.find(e => ( !e.isRedacted() && e.isRelation(THREAD_RELATION_TYPE.name) - )) ?? this.rootEvent; + )) ?? this.rootEvent!; this.emit(ThreadEvent.Update, this); }; @@ -267,7 +267,7 @@ export class Thread extends ReadReceipt { this.client.decryptEventIfNeeded(event, {}); } else if (!toStartOfTimeline && this.initialEventsFetched && - event.localTimestamp > this.lastReply()?.localTimestamp + event.localTimestamp > this.lastReply()!.localTimestamp ) { this.fetchEditsWhereNeeded(event); this.addEventToTimeline(event, false); @@ -289,7 +289,7 @@ export class Thread extends ReadReceipt { } } - private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship { + private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship | undefined { return rootEvent?.getServerAggregatedRelation(THREAD_RELATION_TYPE.name); } @@ -302,7 +302,7 @@ export class Thread extends ReadReceipt { if (Thread.hasServerSideSupport && bundledRelationship) { this.replyCount = bundledRelationship.count; - this._currentUserParticipated = bundledRelationship.current_user_participated; + this._currentUserParticipated = !!bundledRelationship.current_user_participated; const event = new MatrixEvent({ room_id: this.rootEvent.getRoomId(), @@ -407,7 +407,7 @@ export class Thread extends ReadReceipt { } public async fetchEvents(opts: IRelationsRequestOpts = { limit: 20, dir: Direction.Backward }): Promise<{ - originalEvent: MatrixEvent; + originalEvent?: MatrixEvent; events: MatrixEvent[]; nextBatch?: string | null; prevBatch?: string; @@ -427,7 +427,7 @@ export class Thread extends ReadReceipt { // When there's no nextBatch returned with a `from` request we have reached // the end of the thread, and therefore want to return an empty one - if (!opts.to && !nextBatch) { + if (!opts.to && !nextBatch && originalEvent) { events = [...events, originalEvent]; } diff --git a/src/pushprocessor.ts b/src/pushprocessor.ts index 1caa7895332..d89eae4b031 100644 --- a/src/pushprocessor.ts +++ b/src/pushprocessor.ts @@ -184,7 +184,7 @@ export class PushProcessor { private static cachedGlobToRegex: Record = {}; // $glob: RegExp - private matchingRuleFromKindSet(ev: MatrixEvent, kindset: PushRuleSet): IAnnotatedPushRule { + private matchingRuleFromKindSet(ev: MatrixEvent, kindset: PushRuleSet): IAnnotatedPushRule | null { for (let ruleKindIndex = 0; ruleKindIndex < RULEKINDS_IN_ORDER.length; ++ruleKindIndex) { const kind = RULEKINDS_IN_ORDER[ruleKindIndex]; const ruleset = kindset[kind]; @@ -338,19 +338,19 @@ export class PushProcessor { private eventFulfillsDisplayNameCondition(cond: IContainsDisplayNameCondition, ev: MatrixEvent): boolean { let content = ev.getContent(); if (ev.isEncrypted() && ev.getClearContent()) { - content = ev.getClearContent(); + content = ev.getClearContent()!; } if (!content || !content.body || typeof content.body != 'string') { return false; } const room = this.client.getRoom(ev.getRoomId()); - if (!room || !room.currentState || !room.currentState.members || - !room.currentState.getMember(this.client.credentials.userId)) { + const member = room?.currentState?.getMember(this.client.credentials.userId!); + if (!member) { return false; } - const displayName = room.currentState.getMember(this.client.credentials.userId).name; + const displayName = member.name; // N.B. we can't use \b as it chokes on unicode. however \W seems to be okay // as shorthand for [^0-9A-Za-z_]. @@ -396,7 +396,7 @@ export class PushProcessor { private valueForDottedKey(key: string, ev: MatrixEvent): any { const parts = key.split('.'); - let val; + let val: any; // special-case the first component to deal with encrypted messages const firstPart = parts[0]; @@ -412,7 +412,7 @@ export class PushProcessor { } while (parts.length > 0) { - const thisPart = parts.shift(); + const thisPart = parts.shift()!; if (isNullOrUndefined(val[thisPart])) { return null; } @@ -421,7 +421,7 @@ export class PushProcessor { return val; } - private matchingRuleForEventWithRulesets(ev: MatrixEvent, rulesets): IAnnotatedPushRule { + private matchingRuleForEventWithRulesets(ev: MatrixEvent, rulesets?: IPushRules): IAnnotatedPushRule | null { if (!rulesets) { return null; } @@ -432,7 +432,7 @@ export class PushProcessor { return this.matchingRuleFromKindSet(ev, rulesets.global); } - private pushActionsForEventAndRulesets(ev: MatrixEvent, rulesets): IActionsObject { + private pushActionsForEventAndRulesets(ev: MatrixEvent, rulesets?: IPushRules): IActionsObject { const rule = this.matchingRuleForEventWithRulesets(ev, rulesets); if (!rule) { return {} as IActionsObject; @@ -504,9 +504,9 @@ export class PushProcessor { * @param {string} ruleId The ID of the rule to search for * @return {object} The push rule, or null if no such rule was found */ - public getPushRuleById(ruleId: string): IPushRule { + public getPushRuleById(ruleId: string): IPushRule | null { for (const scope of ['global']) { - if (this.client.pushRules[scope] === undefined) continue; + if (this.client.pushRules?.[scope] === undefined) continue; for (const kind of RULEKINDS_IN_ORDER) { if (this.client.pushRules[scope][kind] === undefined) continue; diff --git a/src/realtime-callbacks.ts b/src/realtime-callbacks.ts index 67b261515c6..7c3218ce028 100644 --- a/src/realtime-callbacks.ts +++ b/src/realtime-callbacks.ts @@ -36,14 +36,16 @@ let count = 0; // the key for our callback with the real global.setTimeout let realCallbackKey: NodeJS.Timeout | number; -// a sorted list of the callbacks to be run. -// each is an object with keys [runAt, func, params, key]. -const callbackList: { +type Callback = { runAt: number; func: (...params: any[]) => void; params: any[]; key: number; -}[] = []; +}; + +// a sorted list of the callbacks to be run. +// each is an object with keys [runAt, func, params, key]. +const callbackList: Callback[] = []; // var debuglog = logger.log.bind(logger); const debuglog = function(...params: any[]) {}; @@ -135,19 +137,19 @@ function scheduleRealCallback(): void { } function runCallbacks(): void { - let cb; + let cb: Callback; const timestamp = Date.now(); debuglog("runCallbacks: now:", timestamp); // get the list of things to call - const callbacksToRun = []; + const callbacksToRun: Callback[] = []; // eslint-disable-next-line while (true) { const first = callbackList[0]; if (!first || first.runAt > timestamp) { break; } - cb = callbackList.shift(); + cb = callbackList.shift()!; debuglog("runCallbacks: popping", cb.key); callbacksToRun.push(cb); } @@ -162,8 +164,7 @@ function runCallbacks(): void { try { cb.func.apply(global, cb.params); } catch (e) { - logger.error("Uncaught exception in callback function", - e.stack || e); + logger.error("Uncaught exception in callback function", e); } } } diff --git a/src/rendezvous/MSC3906Rendezvous.ts b/src/rendezvous/MSC3906Rendezvous.ts index a24216751df..d887c4be494 100644 --- a/src/rendezvous/MSC3906Rendezvous.ts +++ b/src/rendezvous/MSC3906Rendezvous.ts @@ -203,13 +203,13 @@ export class MSC3906Rendezvous { true, false, true, ); - const masterPublicKey = this.client.crypto.crossSigningInfo.getId('master'); + const masterPublicKey = this.client.crypto.crossSigningInfo.getId('master')!; await this.send({ type: PayloadType.Finish, outcome: Outcome.Verified, verifying_device_id: this.client.getDeviceId(), - verifying_device_key: this.client.getDeviceEd25519Key(), + verifying_device_key: this.client.getDeviceEd25519Key()!, master_key: masterPublicKey, }); diff --git a/src/room-hierarchy.ts b/src/room-hierarchy.ts index 1acf10e586d..6d95db2a5f9 100644 --- a/src/room-hierarchy.ts +++ b/src/room-hierarchy.ts @@ -22,6 +22,7 @@ import { Room } from "./models/room"; import { IHierarchyRoom, IHierarchyRelation } from "./@types/spaces"; import { MatrixClient } from "./client"; import { EventType } from "./@types/event"; +import { MatrixError } from "./http-api"; export class RoomHierarchy { // Map from room id to list of servers which are listed as a via somewhere in the loaded hierarchy @@ -30,7 +31,7 @@ export class RoomHierarchy { public readonly backRefs = new Map(); // Map from room id to object public readonly roomMap = new Map(); - private loadRequest: ReturnType; + private loadRequest?: ReturnType; private nextBatch?: string; private _rooms?: IHierarchyRoom[]; private serverSupportError?: Error; @@ -65,7 +66,7 @@ export class RoomHierarchy { return !!this.loadRequest; } - public get rooms(): IHierarchyRoom[] { + public get rooms(): IHierarchyRoom[] | undefined { return this._rooms; } @@ -84,15 +85,15 @@ export class RoomHierarchy { try { ({ rooms, next_batch: this.nextBatch } = await this.loadRequest); } catch (e) { - if (e.errcode === "M_UNRECOGNIZED") { - this.serverSupportError = e; + if ((e).errcode === "M_UNRECOGNIZED") { + this.serverSupportError = e; } else { throw e; } return []; } finally { - this.loadRequest = null; + this.loadRequest = undefined; } if (this._rooms) { @@ -112,14 +113,14 @@ export class RoomHierarchy { if (!this.backRefs.has(childRoomId)) { this.backRefs.set(childRoomId, []); } - this.backRefs.get(childRoomId).push(room.room_id); + this.backRefs.get(childRoomId)!.push(room.room_id); // fill viaMap if (Array.isArray(ev.content.via)) { if (!this.viaMap.has(childRoomId)) { this.viaMap.set(childRoomId, new Set()); } - const vias = this.viaMap.get(childRoomId); + const vias = this.viaMap.get(childRoomId)!; ev.content.via.forEach(via => vias.add(via)); } }); @@ -128,11 +129,11 @@ export class RoomHierarchy { return rooms; } - public getRelation(parentId: string, childId: string): IHierarchyRelation { + public getRelation(parentId: string, childId: string): IHierarchyRelation | undefined { return this.roomMap.get(parentId)?.children_state.find(e => e.state_key === childId); } - public isSuggested(parentId: string, childId: string): boolean { + public isSuggested(parentId: string, childId: string): boolean | undefined { return this.getRelation(parentId, childId)?.content.suggested; } diff --git a/src/scheduler.ts b/src/scheduler.ts index 2131e95c253..7a1a99dedb3 100644 --- a/src/scheduler.ts +++ b/src/scheduler.ts @@ -63,7 +63,7 @@ export class MatrixScheduler { * @see module:scheduler~retryAlgorithm */ // eslint-disable-next-line @typescript-eslint/naming-convention - public static RETRY_BACKOFF_RATELIMIT(event: MatrixEvent, attempts: number, err: MatrixError): number { + public static RETRY_BACKOFF_RATELIMIT(event: MatrixEvent | null, attempts: number, err: MatrixError): number { if (err.httpStatus === 400 || err.httpStatus === 403 || err.httpStatus === 401) { // client error; no amount of retrying with save you now. return -1; @@ -114,7 +114,7 @@ export class MatrixScheduler { // }, ...] private readonly queues: Record[]> = {}; private activeQueues: string[] = []; - private procFn: ProcessFunction = null; + private procFn: ProcessFunction | null = null; constructor( public readonly retryAlgorithm = MatrixScheduler.RETRY_BACKOFF_RATELIMIT, @@ -130,7 +130,7 @@ export class MatrixScheduler { * this array will modify the underlying event in the queue. * @see MatrixScheduler.removeEventFromQueue To remove an event from the queue. */ - public getQueueForEvent(event: MatrixEvent): MatrixEvent[] { + public getQueueForEvent(event: MatrixEvent): MatrixEvent[] | null { const name = this.queueAlgorithm(event); if (!name || !this.queues[name]) { return null; @@ -159,6 +159,7 @@ export class MatrixScheduler { removed = true; return true; } + return false; }); return removed; } @@ -239,7 +240,7 @@ export class MatrixScheduler { // This way enqueued relations/redactions to enqueued events can receive // the remove id of their target before being sent. Promise.resolve().then(() => { - return this.procFn(obj.event); + return this.procFn!(obj.event); }).then((res) => { // remove this from the queue this.removeNextEvent(queueName); @@ -265,18 +266,18 @@ export class MatrixScheduler { }); }; - private peekNextEvent(queueName: string): IQueueEntry { + private peekNextEvent(queueName: string): IQueueEntry | undefined { const queue = this.queues[queueName]; if (!Array.isArray(queue)) { - return null; + return undefined; } return queue[0]; } - private removeNextEvent(queueName: string): IQueueEntry { + private removeNextEvent(queueName: string): IQueueEntry | undefined { const queue = this.queues[queueName]; if (!Array.isArray(queue)) { - return null; + return undefined; } return queue.shift(); } diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index 834b1577563..b5810601076 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -52,7 +52,7 @@ class ExtensionE2EE implements Extension { return ExtensionState.PreProcess; } - public onRequest(isInitial: boolean): object { + public onRequest(isInitial: boolean): object | undefined { if (!isInitial) { return undefined; } @@ -90,7 +90,7 @@ class ExtensionE2EE implements Extension { } class ExtensionToDevice implements Extension { - private nextBatch?: string = null; + private nextBatch: string | null = null; constructor(private readonly client: MatrixClient) {} @@ -114,7 +114,7 @@ class ExtensionToDevice implements Extension { } public async onResponse(data: object): Promise { - const cancelledKeyVerificationTxns = []; + const cancelledKeyVerificationTxns: string[] = []; data["events"] = data["events"] || []; data["events"] .map(this.client.getEventMapper()) @@ -125,7 +125,7 @@ class ExtensionToDevice implements Extension { // so we can flag the verification events as cancelled in the loop // below. if (toDeviceEvent.getType() === "m.key.verification.cancel") { - const txnId = toDeviceEvent.getContent()['transaction_id']; + const txnId: string | undefined = toDeviceEvent.getContent()['transaction_id']; if (txnId) { cancelledKeyVerificationTxns.push(txnId); } @@ -177,7 +177,7 @@ class ExtensionAccountData implements Extension { return ExtensionState.PostProcess; } - public onRequest(isInitial: boolean): object { + public onRequest(isInitial: boolean): object | undefined { if (!isInitial) { return undefined; } @@ -235,9 +235,9 @@ class ExtensionAccountData implements Extension { * sliding sync API, see sliding-sync.ts or the class SlidingSync. */ export class SlidingSyncSdk { - private syncState: SyncState = null; - private syncStateData: ISyncStateData; - private lastPos: string = null; + private syncState: SyncState | null = null; + private syncStateData?: ISyncStateData; + private lastPos: string | null = null; private failCount = 0; private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response @@ -259,7 +259,7 @@ export class SlidingSyncSdk { } if (client.getNotifTimelineSet()) { - client.reEmitter.reEmit(client.getNotifTimelineSet(), [ + client.reEmitter.reEmit(client.getNotifTimelineSet()!, [ RoomEvent.Timeline, RoomEvent.TimelineReset, ]); @@ -293,7 +293,7 @@ export class SlidingSyncSdk { this.processRoomData(this.client, room, roomData); } - private onLifecycle(state: SlidingSyncState, resp: MSC3575SlidingSyncResponse | null, err: Error | null): void { + private onLifecycle(state: SlidingSyncState, resp: MSC3575SlidingSyncResponse | null, err?: Error): void { if (err) { logger.debug("onLifecycle", state, err); } @@ -306,7 +306,7 @@ export class SlidingSyncSdk { // Element won't stop showing the initial loading spinner unless we fire SyncState.Prepared if (!this.lastPos) { this.updateSyncState(SyncState.Prepared, { - oldSyncToken: this.lastPos, + oldSyncToken: undefined, nextSyncToken: resp.pos, catchingUp: false, fromCache: false, @@ -315,7 +315,7 @@ export class SlidingSyncSdk { // Conversely, Element won't show the room list unless there is at least 1x SyncState.Syncing // so hence for the very first sync we will fire prepared then immediately syncing. this.updateSyncState(SyncState.Syncing, { - oldSyncToken: this.lastPos, + oldSyncToken: this.lastPos!, nextSyncToken: resp.pos, catchingUp: false, fromCache: false, @@ -357,7 +357,7 @@ export class SlidingSyncSdk { * store. */ public async peek(_roomId: string): Promise { - return null; // TODO + return null!; // TODO } /** @@ -373,7 +373,7 @@ export class SlidingSyncSdk { * @see module:client~MatrixClient#event:"sync" * @return {?String} */ - public getSyncState(): SyncState { + public getSyncState(): SyncState | null { return this.syncState; } @@ -385,8 +385,8 @@ export class SlidingSyncSdk { * this object. * @return {?Object} */ - public getSyncStateData(): ISyncStateData { - return this.syncStateData; + public getSyncStateData(): ISyncStateData | null { + return this.syncStateData ?? null; } private shouldAbortSync(error: MatrixError): boolean { @@ -500,8 +500,7 @@ export class SlidingSyncSdk { if (roomData.initial) { // set the back-pagination token. Do this *before* adding any // events so that clients can start back-paginating. - room.getLiveTimeline().setPaginationToken( - roomData.prev_batch, EventTimeline.BACKWARDS); + room.getLiveTimeline().setPaginationToken(roomData.prev_batch ?? null, EventTimeline.BACKWARDS); } /* TODO @@ -687,7 +686,7 @@ export class SlidingSyncSdk { // slightly naughty by doctoring the invite event but this means all // the code paths remain the same between invite/join display name stuff // which is a worthy trade-off for some minor pollution. - const inviteEvent = member.events.member; + const inviteEvent = member.events.member!; if (inviteEvent.getContent().membership !== "invite") { // between resolving and now they have since joined, so don't clobber return; @@ -723,7 +722,7 @@ export class SlidingSyncSdk { break; } catch (err) { logger.error("Getting push rules failed", err); - if (this.shouldAbortSync(err)) { + if (this.shouldAbortSync(err)) { return; } } @@ -787,7 +786,7 @@ export class SlidingSyncSdk { return a.getTs() - b.getTs(); }); this.notifEvents.forEach((event) => { - this.client.getNotifTimelineSet().addLiveEvent(event); + this.client.getNotifTimelineSet()?.addLiveEvent(event); }); this.notifEvents = []; } @@ -815,7 +814,7 @@ function ensureNameEvent(client: MatrixClient, roomId: string, roomData: MSC3575 content: { name: roomData.name, }, - sender: client.getUserId(), + sender: client.getUserId()!, origin_server_ts: new Date().getTime(), }); return roomData; @@ -824,7 +823,7 @@ function ensureNameEvent(client: MatrixClient, roomId: string, roomData: MSC3575 // Helper functions which set up JS SDK structs are below and are identical to the sync v2 counterparts, // just outside the class. -function mapEvents(client: MatrixClient, roomId: string, events: object[], decrypt = true): MatrixEvent[] { +function mapEvents(client: MatrixClient, roomId: string | undefined, events: object[], decrypt = true): MatrixEvent[] { const mapper = client.getEventMapper({ decrypt }); return (events as Array).map(function(e) { e["room_id"] = roomId; diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index 480ee818cf2..494cfb50578 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -19,6 +19,7 @@ import { MatrixClient } from "./client"; import { IRoomEvent, IStateEvent } from "./sync-accumulator"; import { TypedEventEmitter } from "./models/typed-event-emitter"; import { sleep, IDeferred, defer } from "./utils"; +import { HTTPError } from "./http-api"; // /sync requests allow you to set a timeout= but the request may continue // beyond that and wedge forever, so we need to track how long we are willing @@ -153,12 +154,12 @@ export enum SlidingSyncState { * multiple sliding windows, and maintains the index->room_id mapping. */ class SlidingList { - private list: MSC3575List; - private isModified: boolean; + private list!: MSC3575List; + private isModified?: boolean; // returned data - public roomIndexToRoomId: Record; - public joinedCount: number; + public roomIndexToRoomId: Record = {}; + public joinedCount = 0; /** * Construct a new sliding list. @@ -271,7 +272,7 @@ export interface Extension { * @param isInitial True when this is part of the initial request (send sticky params) * @returns The request JSON to send. */ - onRequest(isInitial: boolean): object; + onRequest(isInitial: boolean): object | undefined; /** * A function which is called when there is response JSON under this extension. * @param data The response JSON under the extension name. @@ -322,7 +323,7 @@ export enum SlidingSyncEvent { export type SlidingSyncEventHandlerMap = { [SlidingSyncEvent.RoomData]: (roomId: string, roomData: MSC3575RoomData) => void; [SlidingSyncEvent.Lifecycle]: ( - state: SlidingSyncState, resp: MSC3575SlidingSyncResponse | null, err: Error | null, + state: SlidingSyncState, resp: MSC3575SlidingSyncResponse | null, err?: Error, ) => void; [SlidingSyncEvent.List]: ( listIndex: number, joinedCount: number, roomIndexToRoomId: Record, @@ -342,7 +343,7 @@ export class SlidingSync extends TypedEventEmitter & { txnId: string})[] = []; @@ -387,7 +388,7 @@ export class SlidingSync extends TypedEventEmitter} { + public getListData(index: number): {joinedCount: number, roomIndexToRoomId: Record} | null { if (!this.lists[index]) { return null; } @@ -403,7 +404,7 @@ export class SlidingSync extends TypedEventEmittererr).httpStatus) { this.invokeLifecycleListeners( SlidingSyncState.RequestFinished, null, - err, + err, ); - if (err.httpStatus === 400) { + if ((err).httpStatus === 400) { // session probably expired TODO: assign an errcode // so drop state and re-request this.resetup(); @@ -851,7 +856,7 @@ export class SlidingSync extends TypedEventEmittererr).name === "AbortError") { continue; // don't sleep as we caused this error by abort()ing the request. } logger.error(err); @@ -865,7 +870,7 @@ export class SlidingSync extends TypedEventEmitter { this.invokeRoomDataListeners( roomId, - resp.rooms[roomId], + resp!.rooms[roomId], ); }); diff --git a/src/store/index.ts b/src/store/index.ts index d58b5819338..c8df0346e30 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -150,7 +150,7 @@ export interface IStore { * @param {string} filterName * @param {string} filterId */ - setFilterIdByName(filterName: string, filterId: string): void; + setFilterIdByName(filterName: string, filterId?: string): void; /** * Store user-scoped account data events diff --git a/src/store/memory.ts b/src/store/memory.ts index 061b1336a97..4c0fb7c08da 100644 --- a/src/store/memory.ts +++ b/src/store/memory.ts @@ -274,14 +274,14 @@ export class MemoryStore implements IStore { * @param {string} filterName * @param {string} filterId */ - public setFilterIdByName(filterName: string, filterId: string) { + public setFilterIdByName(filterName: string, filterId?: string) { if (!this.localStorage) { return; } const key = "mxjssdk_memory_filter_" + filterName; try { if (isValidFilterId(filterId)) { - this.localStorage.setItem(key, filterId); + this.localStorage.setItem(key, filterId!); } else { this.localStorage.removeItem(key); } diff --git a/src/store/stub.ts b/src/store/stub.ts index eb988a9733b..7f4b8c07088 100644 --- a/src/store/stub.ts +++ b/src/store/stub.ts @@ -36,7 +36,7 @@ import { IndexedToDeviceBatch, ToDeviceBatch } from "../models/ToDeviceMessage"; */ export class StubStore implements IStore { public readonly accountData = {}; // stub - private fromToken: string = null; + private fromToken: string | null = null; /** @return {Promise} whether or not the database was newly created in this session. */ public isNewlyCreated(): Promise { @@ -170,7 +170,7 @@ export class StubStore implements IStore { * @param {string} filterName * @param {string} filterId */ - public setFilterIdByName(filterName: string, filterId: string) {} + public setFilterIdByName(filterName: string, filterId?: string) {} /** * Store user-scoped account data events @@ -223,7 +223,7 @@ export class StubStore implements IStore { * client state to where it was at the last save, or null if there * is no saved sync data. */ - public getSavedSync(): Promise { + public getSavedSync(): Promise { return Promise.resolve(null); } @@ -244,7 +244,7 @@ export class StubStore implements IStore { return Promise.resolve(); } - public getOutOfBandMembers(): Promise { + public getOutOfBandMembers(): Promise { return Promise.resolve(null); } diff --git a/src/sync-accumulator.ts b/src/sync-accumulator.ts index ec60c1c3a73..ebb9fb21994 100644 --- a/src/sync-accumulator.ts +++ b/src/sync-accumulator.ts @@ -191,7 +191,7 @@ export class SyncAccumulator { // accumulated. We remember this so that any caller can obtain a // coherent /sync response and know at what point they should be // streaming from without losing events. - private nextBatch: string = null; + private nextBatch: string | null = null; /** * @param {Object} opts @@ -384,8 +384,8 @@ export class SyncAccumulator { if (data.unread_notifications) { currentData._unreadNotifications = data.unread_notifications; } - currentData._unreadThreadNotifications = data[UNREAD_THREAD_NOTIFICATIONS.stable] - ?? data[UNREAD_THREAD_NOTIFICATIONS.unstable] + currentData._unreadThreadNotifications = data[UNREAD_THREAD_NOTIFICATIONS.stable!] + ?? data[UNREAD_THREAD_NOTIFICATIONS.unstable!] ?? undefined; if (data.summary) { @@ -429,7 +429,7 @@ export class SyncAccumulator { Object.entries(e.content[eventId]).forEach(([key, value]) => { if (!isSupportedReceiptType(key)) return; - Object.keys(value).forEach((userId) => { + Object.keys(value!).forEach((userId) => { // clobber on user ID currentData._readReceipts[userId] = { data: e.content[eventId][key][userId], @@ -477,16 +477,16 @@ export class SyncAccumulator { currentData._timeline.push({ event: transformedEvent, - token: index === 0 ? data.timeline.prev_batch : null, + token: index === 0 ? (data.timeline.prev_batch ?? null) : null, }); }); } // attempt to prune the timeline by jumping between events which have // pagination tokens. - if (currentData._timeline.length > this.opts.maxTimelineEntries) { + if (currentData._timeline.length > this.opts.maxTimelineEntries!) { const startIndex = ( - currentData._timeline.length - this.opts.maxTimelineEntries + currentData._timeline.length - this.opts.maxTimelineEntries! ); for (let i = startIndex; i < currentData._timeline.length; i++) { if (currentData._timeline[i].token) { @@ -657,14 +657,14 @@ export class SyncAccumulator { }); return { - nextBatch: this.nextBatch, + nextBatch: this.nextBatch!, roomsData: data, accountData: accData, }; } public getNextBatchToken(): string { - return this.nextBatch; + return this.nextBatch!; } } diff --git a/src/sync.ts b/src/sync.ts index ca45176d810..1669f2d54dc 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -33,7 +33,7 @@ import { Filter } from "./filter"; import { EventTimeline } from "./models/event-timeline"; import { PushProcessor } from "./pushprocessor"; import { logger } from './logger'; -import { InvalidStoreError } from './errors'; +import { InvalidStoreError, InvalidStoreState } from './errors'; import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } from "./client"; import { IEphemeral, @@ -117,7 +117,7 @@ interface ISyncOptions { } export interface ISyncStateData { - error?: MatrixError; + error?: Error; oldSyncToken?: string; nextSyncToken?: string; catchingUp?: boolean; @@ -163,14 +163,14 @@ type WrappedRoom = T & { */ export class SyncApi { private _peekRoom: Optional = null; - private currentSyncRequest: Optional> = null; + private currentSyncRequest?: Promise; private abortController?: AbortController; - private syncState: Optional = null; - private syncStateData: Optional = null; // additional data (eg. error object for failed sync) + private syncState: SyncState | null = null; + private syncStateData?: ISyncStateData; // additional data (eg. error object for failed sync) private catchingUp = false; private running = false; - private keepAliveTimer: Optional> = null; - private connectionReturnedDefer: Optional> = null; + private keepAliveTimer?: ReturnType; + private connectionReturnedDefer?: IDeferred; private notifEvents: MatrixEvent[] = []; // accumulator of sync events in the current sync response private failedSyncCount = 0; // Number of consecutive failed /sync requests private storeIsInvalid = false; // flag set if the store needs to be cleared before we can start @@ -189,7 +189,7 @@ export class SyncApi { } if (client.getNotifTimelineSet()) { - client.reEmitter.reEmit(client.getNotifTimelineSet(), [ + client.reEmitter.reEmit(client.getNotifTimelineSet()!, [ RoomEvent.Timeline, RoomEvent.TimelineReset, ]); @@ -281,7 +281,7 @@ export class SyncApi { * Sync rooms the user has left. * @return {Promise} Resolved when they've been added to the store. */ - public syncLeftRooms() { + public async syncLeftRooms(): Promise { const client = this.client; // grab a filter with limit=1 and include_leave=true @@ -289,55 +289,62 @@ export class SyncApi { filter.setTimelineLimit(1); filter.setIncludeLeaveRooms(true); - const localTimeoutMs = this.opts.pollTimeout + BUFFER_PERIOD_MS; + const localTimeoutMs = this.opts.pollTimeout! + BUFFER_PERIOD_MS; + + const filterId = await client.getOrCreateFilter( + getFilterName(client.credentials.userId!, "LEFT_ROOMS"), filter, + ); + const qps: ISyncParams = { timeout: 0, // don't want to block since this is a single isolated req + filter: filterId, }; - return client.getOrCreateFilter( - getFilterName(client.credentials.userId, "LEFT_ROOMS"), filter, - ).then(function(filterId) { - qps.filter = filterId; - return client.http.authedRequest(Method.Get, "/sync", qps as any, undefined, { - localTimeoutMs, - }); - }).then(async (data) => { - let leaveRooms = []; - if (data.rooms?.leave) { - leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave); + const data = await client.http.authedRequest(Method.Get, "/sync", qps as any, undefined, { + localTimeoutMs, + }); + + let leaveRooms: WrappedRoom[] = []; + if (data.rooms?.leave) { + leaveRooms = this.mapSyncResponseToRoomArray(data.rooms.leave); + } + + const rooms = await Promise.all(leaveRooms.map(async (leaveObj) => { + const room = leaveObj.room; + if (!leaveObj.isBrandNewRoom) { + // the intention behind syncLeftRooms is to add in rooms which were + // *omitted* from the initial /sync. Rooms the user were joined to + // but then left whilst the app is running will appear in this list + // and we do not want to bother with them since they will have the + // current state already (and may get dupe messages if we add + // yet more timeline events!), so skip them. + // NB: When we persist rooms to localStorage this will be more + // complicated... + return; } - return Promise.all(leaveRooms.map(async (leaveObj) => { - const room = leaveObj.room; - if (!leaveObj.isBrandNewRoom) { - // the intention behind syncLeftRooms is to add in rooms which were - // *omitted* from the initial /sync. Rooms the user were joined to - // but then left whilst the app is running will appear in this list - // and we do not want to bother with them since they will have the - // current state already (and may get dupe messages if we add - // yet more timeline events!), so skip them. - // NB: When we persist rooms to localStorage this will be more - // complicated... - return; - } - leaveObj.timeline = leaveObj.timeline || {}; - const events = this.mapSyncEventsFormat(leaveObj.timeline, room); + leaveObj.timeline = leaveObj.timeline || { + prev_batch: null, + events: [], + }; + const events = this.mapSyncEventsFormat(leaveObj.timeline, room); - const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room); + const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room); - // set the back-pagination token. Do this *before* adding any - // events so that clients can start back-paginating. - room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, EventTimeline.BACKWARDS); + // set the back-pagination token. Do this *before* adding any + // events so that clients can start back-paginating. + room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, EventTimeline.BACKWARDS); - await this.processRoomEvents(room, stateEvents, events); + await this.processRoomEvents(room, stateEvents, events); - room.recalculate(); - client.store.storeRoom(room); - client.emit(ClientEvent.Room, room); + room.recalculate(); + client.store.storeRoom(room); + client.emit(ClientEvent.Room, room); - this.processEventsForNotifs(room, events); - return room; - })); - }); + this.processEventsForNotifs(room, events); + return room; + })); + + return rooms.filter(Boolean) as Room[]; } /** @@ -348,7 +355,7 @@ export class SyncApi { * store. */ public peek(roomId: string): Promise { - if (this._peekRoom && this._peekRoom.roomId === roomId) { + if (this._peekRoom?.roomId === roomId) { return Promise.resolve(this._peekRoom); } @@ -388,28 +395,28 @@ export class SyncApi { // fire off pagination requests in response to the Room.timeline // events. if (response.messages.start) { - this._peekRoom.oldState.paginationToken = response.messages.start; + this._peekRoom!.oldState.paginationToken = response.messages.start; } // set the state of the room to as it was after the timeline executes - this._peekRoom.oldState.setStateEvents(oldStateEvents); - this._peekRoom.currentState.setStateEvents(stateEvents); + this._peekRoom!.oldState.setStateEvents(oldStateEvents); + this._peekRoom!.currentState.setStateEvents(stateEvents); - this.resolveInvites(this._peekRoom); - this._peekRoom.recalculate(); + this.resolveInvites(this._peekRoom!); + this._peekRoom!.recalculate(); // roll backwards to diverge old state. addEventsToTimeline // will overwrite the pagination token, so make sure it overwrites // it with the right thing. - this._peekRoom.addEventsToTimeline(messages.reverse(), true, - this._peekRoom.getLiveTimeline(), + this._peekRoom!.addEventsToTimeline(messages.reverse(), true, + this._peekRoom!.getLiveTimeline(), response.messages.start); - client.store.storeRoom(this._peekRoom); - client.emit(ClientEvent.Room, this._peekRoom); + client.store.storeRoom(this._peekRoom!); + client.emit(ClientEvent.Room, this._peekRoom!); - this.peekPoll(this._peekRoom); - return this._peekRoom; + this.peekPoll(this._peekRoom!); + return this._peekRoom!; }); } @@ -437,7 +444,10 @@ export class SyncApi { room_id: peekRoom.roomId, timeout: String(30 * 1000), from: token, - }, undefined, { localTimeoutMs: 50 * 1000 }).then((res) => { + }, undefined, { + localTimeoutMs: 50 * 1000, + abortSignal: this.abortController?.signal, + }).then((res) => { if (this._peekRoom !== peekRoom) { debuglog("Stopped peeking in room %s", peekRoom.roomId); return; @@ -487,7 +497,7 @@ export class SyncApi { * @see module:client~MatrixClient#event:"sync" * @return {?String} */ - public getSyncState(): SyncState { + public getSyncState(): SyncState | null { return this.syncState; } @@ -499,18 +509,18 @@ export class SyncApi { * this object. * @return {?Object} */ - public getSyncStateData(): ISyncStateData { - return this.syncStateData; + public getSyncStateData(): ISyncStateData | null { + return this.syncStateData ?? null; } - public async recoverFromSyncStartupError(savedSyncPromise: Promise, err: MatrixError): Promise { + public async recoverFromSyncStartupError(savedSyncPromise: Promise | undefined, error: Error): Promise { // Wait for the saved sync to complete - we send the pushrules and filter requests // before the saved sync has finished so they can run in parallel, but only process // the results after the saved sync is done. Equivalently, we wait for it to finish // before reporting failures from these functions. await savedSyncPromise; const keepaliveProm = this.startKeepAlives(); - this.updateSyncState(SyncState.Error, { error: err }); + this.updateSyncState(SyncState.Error, { error }); await keepaliveProm; } @@ -553,11 +563,11 @@ export class SyncApi { this.client.pushRules = result; } catch (err) { logger.error("Getting push rules failed", err); - if (this.shouldAbortSync(err)) return; + if (this.shouldAbortSync(err)) return; // wait for saved sync to complete before doing anything else, // otherwise the sync state will end up being incorrect debuglog("Waiting for saved sync before retrying push rules..."); - await this.recoverFromSyncStartupError(this.savedSyncPromise, err); + await this.recoverFromSyncStartupError(this.savedSyncPromise, err); return this.getPushRules(); // try again } }; @@ -595,8 +605,7 @@ export class SyncApi { const shouldClear = await this.wasLazyLoadingToggled(this.opts.lazyLoadMembers); if (shouldClear) { this.storeIsInvalid = true; - const reason = InvalidStoreError.TOGGLED_LAZY_LOADING; - const error = new InvalidStoreError(reason, !!this.opts.lazyLoadMembers); + const error = new InvalidStoreError(InvalidStoreState.ToggledLazyLoading, !!this.opts.lazyLoadMembers); this.updateSyncState(SyncState.Error, { error }); // bail out of the sync loop now: the app needs to respond to this error. // we leave the state as 'ERROR' which isn't great since this normally means @@ -632,20 +641,20 @@ export class SyncApi { let filterId: string; try { - filterId = await this.client.getOrCreateFilter(getFilterName(this.client.credentials.userId), filter); + filterId = await this.client.getOrCreateFilter(getFilterName(this.client.credentials.userId!), filter); } catch (err) { logger.error("Getting filter failed", err); - if (this.shouldAbortSync(err)) return {}; + if (this.shouldAbortSync(err)) return {}; // wait for saved sync to complete before doing anything else, // otherwise the sync state will end up being incorrect debuglog("Waiting for saved sync before retrying filter..."); - await this.recoverFromSyncStartupError(this.savedSyncPromise, err); + await this.recoverFromSyncStartupError(this.savedSyncPromise, err); return this.getFilter(); // try again } return { filter, filterId }; }; - private savedSyncPromise: Promise; + private savedSyncPromise?: Promise; /** * Main entry point @@ -701,7 +710,7 @@ export class SyncApi { // /notifications API somehow. this.client.resetNotifTimelineSet(); - if (this.currentSyncRequest === null) { + if (!this.currentSyncRequest) { let firstSyncFilter = filterId; const savedSyncToken = await savedSyncTokenPromise; @@ -711,7 +720,7 @@ export class SyncApi { debuglog("Sending initial sync request..."); const initialFilter = this.buildDefaultFilter(); initialFilter.setDefinition(filter.getDefinition()); - initialFilter.setTimelineLimit(this.opts.initialSyncLimit); + initialFilter.setTimelineLimit(this.opts.initialSyncLimit!); // Use an inline filter, no point uploading it for a single usage firstSyncFilter = JSON.stringify(initialFilter.getDefinition()); } @@ -742,7 +751,7 @@ export class SyncApi { this.abortController?.abort(); if (this.keepAliveTimer) { clearTimeout(this.keepAliveTimer); - this.keepAliveTimer = null; + this.keepAliveTimer = undefined; } } @@ -813,16 +822,16 @@ export class SyncApi { let data: ISyncResponse; try { //debuglog('Starting sync since=' + syncToken); - if (this.currentSyncRequest === null) { + if (!this.currentSyncRequest) { this.currentSyncRequest = this.doSyncRequest(syncOptions, syncToken); } data = await this.currentSyncRequest; } catch (e) { - const abort = await this.onSyncError(e); + const abort = await this.onSyncError(e); if (abort) return; continue; } finally { - this.currentSyncRequest = null; + this.currentSyncRequest = undefined; } //debuglog('Completed sync, next_batch=' + data.next_batch); @@ -838,7 +847,7 @@ export class SyncApi { await this.client.store.setSyncData(data); const syncEventData = { - oldSyncToken: syncToken, + oldSyncToken: syncToken ?? undefined, nextSyncToken: data.next_batch, catchingUp: this.catchingUp, }; @@ -857,7 +866,7 @@ export class SyncApi { logger.error("Caught /sync error", e); // Emit the exception for client handling - this.client.emit(ClientEvent.SyncUnexpectedError, e); + this.client.emit(ClientEvent.SyncUnexpectedError, e); } // update this as it may have changed @@ -897,13 +906,13 @@ export class SyncApi { debuglog("Sync no longer running: exiting."); if (this.connectionReturnedDefer) { this.connectionReturnedDefer.reject(); - this.connectionReturnedDefer = null; + this.connectionReturnedDefer = undefined; } this.updateSyncState(SyncState.Stopped); } } - private doSyncRequest(syncOptions: ISyncOptions, syncToken: string): Promise { + private doSyncRequest(syncOptions: ISyncOptions, syncToken: string | null): Promise { const qps = this.getSyncParams(syncOptions, syncToken); return this.client.http.authedRequest(Method.Get, "/sync", qps as any, undefined, { localTimeoutMs: qps.timeout + BUFFER_PERIOD_MS, @@ -911,8 +920,8 @@ export class SyncApi { }); } - private getSyncParams(syncOptions: ISyncOptions, syncToken: string): ISyncParams { - let pollTimeout = this.opts.pollTimeout; + private getSyncParams(syncOptions: ISyncOptions, syncToken: string | null): ISyncParams { + let timeout = this.opts.pollTimeout!; if (this.getSyncState() !== SyncState.Syncing || this.catchingUp) { // unless we are happily syncing already, we want the server to return @@ -927,7 +936,7 @@ export class SyncApi { // for us. We do that by calling it with a zero timeout until it // doesn't give us any more to_device messages. this.catchingUp = true; - pollTimeout = 0; + timeout = 0; } let filter = syncOptions.filter; @@ -935,10 +944,7 @@ export class SyncApi { filter = this.getGuestFilter(); } - const qps: ISyncParams = { - filter, - timeout: pollTimeout, - }; + const qps: ISyncParams = { filter, timeout }; if (this.opts.disablePresence) { qps.set_presence = SetPresence.Offline; @@ -953,7 +959,7 @@ export class SyncApi { qps._cacheBuster = Date.now(); } - if ([SyncState.Reconnecting, SyncState.Error].includes(this.getSyncState())) { + if ([SyncState.Reconnecting, SyncState.Error].includes(this.getSyncState()!)) { // we think the connection is dead. If it comes back up, we won't know // about it till /sync returns. If the timeout= is high, this could // be a long time. Set it to 0 when doing retries so we don't have to wait @@ -969,7 +975,7 @@ export class SyncApi { debuglog("Sync no longer running: exiting"); if (this.connectionReturnedDefer) { this.connectionReturnedDefer.reject(); - this.connectionReturnedDefer = null; + this.connectionReturnedDefer = undefined; } this.updateSyncState(SyncState.Stopped); return true; // abort @@ -994,7 +1000,7 @@ export class SyncApi { // if they wish. const keepAlivePromise = this.startKeepAlives(); - this.currentSyncRequest = null; + this.currentSyncRequest = undefined; // Transition from RECONNECTING to ERROR after a given number of failed syncs this.updateSyncState( this.failedSyncCount >= FAILED_SYNC_ERROR_THRESHOLD ? @@ -1073,7 +1079,7 @@ export class SyncApi { // handle presence events (User objects) if (Array.isArray(data.presence?.events)) { - data.presence.events.map(client.getEventMapper()).forEach( + data.presence!.events.map(client.getEventMapper()).forEach( function(presenceEvent) { let user = client.store.getUser(presenceEvent.getSender()); if (user) { @@ -1113,9 +1119,9 @@ export class SyncApi { } // handle to-device events - if (Array.isArray(data.to_device?.events) && data.to_device.events.length > 0) { - const cancelledKeyVerificationTxns = []; - data.to_device.events + if (Array.isArray(data.to_device?.events) && data.to_device!.events.length > 0) { + const cancelledKeyVerificationTxns: string[] = []; + data.to_device!.events .filter((eventJSON) => { if ( eventJSON.type === EventType.RoomMessageEncrypted && @@ -1137,7 +1143,7 @@ export class SyncApi { // so we can flag the verification events as cancelled in the loop // below. if (toDeviceEvent.getType() === "m.key.verification.cancel") { - const txnId = toDeviceEvent.getContent()['transaction_id']; + const txnId: string = toDeviceEvent.getContent()['transaction_id']; if (txnId) { cancelledKeyVerificationTxns.push(txnId); } @@ -1206,13 +1212,13 @@ export class SyncApi { await this.processRoomEvents(room, stateEvents); - const inviter = room.currentState.getStateEvents(EventType.RoomMember, client.getUserId())?.getSender(); + const inviter = room.currentState.getStateEvents(EventType.RoomMember, client.getUserId()!)?.getSender(); if (client.isCryptoEnabled()) { - const parkedHistory = await client.crypto.cryptoStore.takeParkedSharedHistory(room.roomId); + const parkedHistory = await client.crypto!.cryptoStore.takeParkedSharedHistory(room.roomId); for (const parked of parkedHistory) { if (parked.senderId === inviter) { - await client.crypto.olmDevice.addInboundGroupSession( + await client.crypto!.olmDevice.addInboundGroupSession( room.roomId, parked.senderKey, parked.forwardingCurve25519KeyChain, @@ -1256,7 +1262,7 @@ export class SyncApi { if (joinObj.unread_notifications) { room.setUnreadNotificationCount( NotificationCountType.Total, - joinObj.unread_notifications.notification_count, + joinObj.unread_notifications.notification_count ?? 0, ); // We track unread notifications ourselves in encrypted rooms, so don't @@ -1266,13 +1272,13 @@ export class SyncApi { if (!encrypted || room.getUnreadNotificationCount(NotificationCountType.Highlight) <= 0) { room.setUnreadNotificationCount( NotificationCountType.Highlight, - joinObj.unread_notifications.highlight_count, + joinObj.unread_notifications.highlight_count ?? 0, ); } } const unreadThreadNotifications = joinObj[UNREAD_THREAD_NOTIFICATIONS.name] - ?? joinObj[UNREAD_THREAD_NOTIFICATIONS.altName]; + ?? joinObj[UNREAD_THREAD_NOTIFICATIONS.altName!]; if (unreadThreadNotifications) { // Only partially reset unread notification // We want to keep the client-generated count. Particularly important @@ -1283,7 +1289,7 @@ export class SyncApi { room.setThreadUnreadNotificationCount( threadId, NotificationCountType.Total, - unreadNotification.notification_count, + unreadNotification.notification_count ?? 0, ); const hasNoNotifications = @@ -1292,7 +1298,7 @@ export class SyncApi { room.setThreadUnreadNotificationCount( threadId, NotificationCountType.Highlight, - unreadNotification.highlight_count, + unreadNotification.highlight_count ?? 0, ); } } @@ -1349,8 +1355,8 @@ export class SyncApi { if (limited) { room.resetLiveTimeline( joinObj.timeline.prev_batch, - this.opts.canResetEntireTimeline(room.roomId) ? - null : syncEventData.oldSyncToken, + this.opts.canResetEntireTimeline!(room.roomId) ? + null : (syncEventData.oldSyncToken ?? null), ); // We have to assume any gap in any timeline is @@ -1448,7 +1454,7 @@ export class SyncApi { return a.getTs() - b.getTs(); }); this.notifEvents.forEach(function(event) { - client.getNotifTimelineSet().addLiveEvent(event); + client.getNotifTimelineSet()?.addLiveEvent(event); }); } @@ -1523,7 +1529,7 @@ export class SyncApi { clearTimeout(this.keepAliveTimer); if (this.connectionReturnedDefer) { this.connectionReturnedDefer.resolve(connDidFail); - this.connectionReturnedDefer = null; + this.connectionReturnedDefer = undefined; } }; @@ -1639,7 +1645,7 @@ export class SyncApi { // the code paths remain the same between invite/join display name stuff // which is a worthy trade-off for some minor pollution. const inviteEvent = member.events.member; - if (inviteEvent.getContent().membership !== "invite") { + if (inviteEvent?.getContent().membership !== "invite") { // between resolving and now they have since joined, so don't clobber return; } @@ -1802,7 +1808,7 @@ function createNewUser(client: MatrixClient, userId: string): User { export function _createAndReEmitRoom(client: MatrixClient, roomId: string, opts: Partial): Room { const { timelineSupport } = client; - const room = new Room(roomId, client, client.getUserId(), { + const room = new Room(roomId, client, client.getUserId()!, { lazyLoadMembers: opts.lazyLoadMembers, pendingEventOrdering: opts.pendingEventOrdering, timelineSupport, @@ -1832,7 +1838,7 @@ export function _createAndReEmitRoom(client: MatrixClient, roomId: string, opts: // We need to add a listener for RoomState.members in order to hook them // correctly. room.on(RoomStateEvent.NewMember, (event, state, member) => { - member.user = client.getUser(member.userId); + member.user = client.getUser(member.userId) ?? undefined; client.reEmitter.reEmit(member, [ RoomMemberEvent.Name, RoomMemberEvent.Typing, diff --git a/src/timeline-window.ts b/src/timeline-window.ts index 24c95fbcf07..ba6a16ea55f 100644 --- a/src/timeline-window.ts +++ b/src/timeline-window.ts @@ -16,6 +16,8 @@ limitations under the License. /** @module timeline-window */ +import { Optional } from "matrix-events-sdk"; + import { Direction, EventTimeline } from './models/event-timeline'; import { logger } from './logger'; import { MatrixClient } from "./client"; @@ -49,8 +51,8 @@ export class TimelineWindow { // 'end' of the window. // // start.index is inclusive; end.index is exclusive. - private start?: TimelineIndex = null; - private end?: TimelineIndex = null; + private start?: TimelineIndex; + private end?: TimelineIndex; private eventCount = 0; /** @@ -102,7 +104,11 @@ export class TimelineWindow { public load(initialEventId?: string, initialWindowSize = 20): Promise { // given an EventTimeline, find the event we were looking for, and initialise our // fields so that the event in question is in the middle of the window. - const initFields = (timeline: EventTimeline) => { + const initFields = (timeline: Optional) => { + if (!timeline) { + throw new Error("No timeline given to initFields"); + } + let eventIndex: number; const events = timeline.getEvents(); @@ -153,11 +159,11 @@ export class TimelineWindow { * @return {TimelineIndex} The requested timeline index if one exists, null * otherwise. */ - public getTimelineIndex(direction: Direction): TimelineIndex { + public getTimelineIndex(direction: Direction): TimelineIndex | null { if (direction == EventTimeline.BACKWARDS) { - return this.start; + return this.start ?? null; } else if (direction == EventTimeline.FORWARDS) { - return this.end; + return this.end ?? null; } else { throw new Error("Invalid direction '" + direction + "'"); } @@ -299,7 +305,7 @@ export class TimelineWindow { backwards: direction == EventTimeline.BACKWARDS, limit: size, }).finally(function() { - tl.pendingPaginate = null; + tl.pendingPaginate = undefined; }).then((r) => { debuglog("TimelineWindow: request completed with result " + r); if (!r) { @@ -334,11 +340,17 @@ export class TimelineWindow { */ public unpaginate(delta: number, startOfTimeline: boolean): void { const tl = startOfTimeline ? this.start : this.end; + if (!tl) { + throw new Error( + `Attempting to unpaginate startOfTimeline=${startOfTimeline} but don't have this direction`, + ); + } // sanity-check the delta if (delta > this.eventCount || delta < 0) { - throw new Error("Attemting to unpaginate " + delta + " events, but " + - "only have " + this.eventCount + " in the timeline"); + throw new Error( + `Attemting to unpaginate ${delta} events, but only have ${this.eventCount} in the timeline`, + ); } while (delta > 0) { @@ -368,7 +380,7 @@ export class TimelineWindow { return []; } - const result = []; + const result: MatrixEvent[] = []; // iterate through each timeline between this.start and this.end // (inclusive). @@ -390,7 +402,7 @@ export class TimelineWindow { if (timeline === this.start.timeline) { startIndex = this.start.index + timeline.getBaseIndex(); } - if (timeline === this.end.timeline) { + if (timeline === this.end?.timeline) { endIndex = this.end.index + timeline.getBaseIndex(); } @@ -399,10 +411,10 @@ export class TimelineWindow { } // if we're not done, iterate to the next timeline. - if (timeline === this.end.timeline) { + if (timeline === this.end?.timeline) { break; } else { - timeline = timeline.getNeighbouringTimeline(EventTimeline.FORWARDS); + timeline = timeline.getNeighbouringTimeline(EventTimeline.FORWARDS)!; } } diff --git a/src/utils.ts b/src/utils.ts index 04a70a64a3d..1e396d2de5d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -48,7 +48,7 @@ export function internaliseString(str: string): string { } // Return any cached string reference - return interns.get(str); + return interns.get(str)!; } /** @@ -412,7 +412,7 @@ export function defer(): IDeferred { export async function promiseMapSeries( promises: Array>, - fn: (t: T) => Promise | void, // if async/promise we don't care about the type as we only await resolution + fn: (t: T) => Promise | undefined, // if async we don't care about the type as we only await resolution ): Promise { for (const o of promises) { await fn(await o); diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 7ced5744c0f..2b0a975fb6f 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -47,6 +47,7 @@ import { CallFeed } from './callFeed'; import { MatrixClient } from "../client"; import { ISendEventResponse } from "../@types/requests"; import { EventEmitterEvents, TypedEventEmitter } from "../models/typed-event-emitter"; +import { MatrixError } from "../http-api"; // events: hangup, error(err), replaced(call), state(state, oldState) @@ -68,7 +69,7 @@ import { EventEmitterEvents, TypedEventEmitter } from "../models/typed-event-emi interface CallOpts { roomId?: string; - client?: any; // Fix when client is TSified + client: MatrixClient; forceTURN?: boolean; turnServers?: Array; } @@ -227,7 +228,7 @@ const FALLBACK_ICE_SERVER = 'stun:turn.matrix.org'; const CALL_TIMEOUT_MS = 60000; export class CallError extends Error { - code: string; + public readonly code: string; constructor(code: CallErrorCode, msg: string, err: Error) { // Still don't think there's any way to have proper nested errors @@ -271,33 +272,33 @@ export class MatrixCall extends TypedEventEmitter; + private readonly client: MatrixClient; + private readonly forceTURN: boolean; + private readonly turnServers: Array; // A queue for candidates waiting to go out. // We try to amalgamate candidates into a single candidate message where // possible private candidateSendQueue: Array = []; private candidateSendTries = 0; private sentEndOfCandidates = false; - private peerConn: RTCPeerConnection; + private peerConn?: RTCPeerConnection; private feeds: Array = []; private usermediaSenders: Array = []; private screensharingSenders: Array = []; private inviteOrAnswerSent = false; private waitForLocalAVStream: boolean; - private successor: MatrixCall; - private opponentMember: RoomMember; - private opponentVersion: number | string; + private successor?: MatrixCall; + private opponentMember?: RoomMember; + private opponentVersion?: number | string; // The party ID of the other side: undefined if we haven't chosen a partner // yet, null if we have but they didn't send a party ID. - private opponentPartyId: string; - private opponentCaps: CallCapabilities; + private opponentPartyId?: string | null; + private opponentCaps?: CallCapabilities; private inviteTimeout?: ReturnType; // The logic of when & if a call is on hold is nontrivial and explained in is*OnHold @@ -307,7 +308,7 @@ export class MatrixCall extends TypedEventEmitter(); - private remoteAssertedIdentity: AssertedIdentity; - - private remoteSDPStreamMetadata: SDPStreamMetadata; + private remoteAssertedIdentity?: AssertedIdentity; + private remoteSDPStreamMetadata?: SDPStreamMetadata; private callLengthInterval?: ReturnType; private callLength = 0; @@ -366,13 +366,13 @@ export class MatrixCall extends TypedEventEmitter 0; + return !!this.localUsermediaStream?.getVideoTracks().length; } public get hasRemoteUserMediaVideoTrack(): boolean { return this.getRemoteFeeds().some((feed) => { - return ( - feed.purpose === SDPStreamMetadataPurpose.Usermedia && - feed.stream.getVideoTracks().length > 0 - ); + return feed.purpose === SDPStreamMetadataPurpose.Usermedia && feed.stream?.getVideoTracks().length; }); } public get hasLocalUserMediaAudioTrack(): boolean { - return this.localUsermediaStream?.getAudioTracks().length > 0; + return !!this.localUsermediaStream?.getAudioTracks().length; } public get hasRemoteUserMediaAudioTrack(): boolean { return this.getRemoteFeeds().some((feed) => { - return ( - feed.purpose === SDPStreamMetadataPurpose.Usermedia && - feed.stream.getAudioTracks().length > 0 - ); + return feed.purpose === SDPStreamMetadataPurpose.Usermedia && !!feed.stream?.getAudioTracks().length; }); } - public get localUsermediaFeed(): CallFeed { + public get localUsermediaFeed(): CallFeed | undefined { return this.getLocalFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Usermedia); } - public get localScreensharingFeed(): CallFeed { + public get localScreensharingFeed(): CallFeed | undefined { return this.getLocalFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Screenshare); } - public get localUsermediaStream(): MediaStream { + public get localUsermediaStream(): MediaStream | undefined { return this.localUsermediaFeed?.stream; } - public get localScreensharingStream(): MediaStream { + public get localScreensharingStream(): MediaStream | undefined { return this.localScreensharingFeed?.stream; } - public get remoteUsermediaFeed(): CallFeed { + public get remoteUsermediaFeed(): CallFeed | undefined { return this.getRemoteFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Usermedia); } - public get remoteScreensharingFeed(): CallFeed { + public get remoteScreensharingFeed(): CallFeed | undefined { return this.getRemoteFeeds().find((feed) => feed.purpose === SDPStreamMetadataPurpose.Screenshare); } - public get remoteUsermediaStream(): MediaStream { + public get remoteUsermediaStream(): MediaStream | undefined { return this.remoteUsermediaFeed?.stream; } - public get remoteScreensharingStream(): MediaStream { + public get remoteScreensharingStream(): MediaStream | undefined { return this.remoteScreensharingFeed?.stream; } - private getFeedByStreamId(streamId: string): CallFeed { + private getFeedByStreamId(streamId: string): CallFeed | undefined { return this.getFeeds().find((feed) => feed.stream.id === streamId); } @@ -513,10 +507,10 @@ export class MatrixCall extends TypedEventEmitter !feed.isLocal())?.stream; @@ -580,7 +574,7 @@ export class MatrixCall extends TypedEventEmitter { + public async getCurrentCallStats(): Promise { if (this.callHasEnded()) { return this.callStatsAtEnd; } @@ -695,7 +689,7 @@ export class MatrixCall extends TypedEventEmitter { + private async collectCallStats(): Promise { // This happens when the call fails before it starts. // For example when we fail to get capture sources if (!this.peerConn) return; @@ -765,8 +759,8 @@ export class MatrixCall extends TypedEventEmittere); return; } } @@ -953,7 +949,7 @@ export class MatrixCall extends TypedEventEmittererror), ); } } @@ -1008,10 +1004,10 @@ export class MatrixCall extends TypedEventEmitter { - return track.kind === "video"; - }); - const sender = this.usermediaSenders.find((sender) => { - return sender.track?.kind === "video"; - }); - sender.replaceTrack(track); + const track = stream.getTracks().find(track => track.kind === "video"); + const sender = this.usermediaSenders.find(sender => sender.track?.kind === "video"); + sender?.replaceTrack(track ?? null); this.pushNewLocalFeed(stream, SDPStreamMetadataPurpose.Screenshare, false); @@ -1048,16 +1040,12 @@ export class MatrixCall extends TypedEventEmitter { - return track.kind === "video"; - }); - const sender = this.usermediaSenders.find((sender) => { - return sender.track?.kind === "video"; - }); - sender.replaceTrack(track); + const track = this.localUsermediaStream?.getTracks().find((track) => track.kind === "video"); + const sender = this.usermediaSenders.find((sender) => sender.track?.kind === "video"); + sender?.replaceTrack(track ?? null); - this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream); - this.deleteFeedByStream(this.localScreensharingStream); + this.client.getMediaHandler().stopScreensharingStream(this.localScreensharingStream!); + this.deleteFeedByStream(this.localScreensharingStream!); return false; } @@ -1070,22 +1058,22 @@ export class MatrixCall extends TypedEventEmitter { - const callFeed = this.localUsermediaFeed; + const callFeed = this.localUsermediaFeed!; const audioEnabled = forceAudio || (!callFeed.isAudioMuted() && !this.remoteOnHold); const videoEnabled = forceVideo || (!callFeed.isVideoMuted() && !this.remoteOnHold); setTracksEnabled(stream.getAudioTracks(), audioEnabled); setTracksEnabled(stream.getVideoTracks(), videoEnabled); // We want to keep the same stream id, so we replace the tracks rather than the whole stream - for (const track of this.localUsermediaStream.getTracks()) { - this.localUsermediaStream.removeTrack(track); + for (const track of this.localUsermediaStream!.getTracks()) { + this.localUsermediaStream!.removeTrack(track); track.stop(); } for (const track of stream.getTracks()) { - this.localUsermediaStream.addTrack(track); + this.localUsermediaStream!.addTrack(track); } - const newSenders = []; + const newSenders: RTCRtpSender[] = []; for (const track of stream.getTracks()) { const oldSender = this.usermediaSenders.find((sender) => sender.track?.kind === track.kind); @@ -1111,7 +1099,7 @@ export class MatrixCall extends TypedEventEmitter { const answerContent = { answer: { - sdp: this.peerConn.localDescription.sdp, + sdp: this.peerConn!.localDescription!.sdp, // type is now deprecated as of Matrix VoIP v1, but // required to still be sent for backwards compat - type: this.peerConn.localDescription.type, + type: this.peerConn!.localDescription!.type, }, [SDPStreamMetadataKey]: this.getLocalSDPStreamMetadata(), } as MCallAnswer; @@ -1305,15 +1293,15 @@ export class MatrixCall extends TypedEventEmittererror).name == 'UnknownDeviceError') { code = CallErrorCode.UnknownDevices; message = "Unknown devices present in the room"; } - this.emit(CallEvent.Error, new CallError(code, message, error)); + this.emit(CallEvent.Error, new CallError(code, message, error)); throw error; } @@ -1333,10 +1321,10 @@ export class MatrixCall extends TypedEventEmitter => { + private gotLocalIceCandidate = (event: RTCPeerConnectionIceEvent): void => { if (event.candidate) { logger.debug( "Call " + this.callId + " got local ICE " + event.candidate.sdpMid + " candidate: " + @@ -1384,8 +1372,8 @@ export class MatrixCall extends TypedEventEmitter { - logger.debug("ice gathering state changed to " + this.peerConn.iceGatheringState); - if (this.peerConn.iceGatheringState === 'complete' && !this.sentEndOfCandidates) { + logger.debug("ice gathering state changed to " + this.peerConn?.iceGatheringState); + if (this.peerConn?.iceGatheringState === 'complete' && !this.sentEndOfCandidates) { // If we didn't get an empty-string candidate to signal the end of candidates, // create one ourselves now gathering has finished. // We cast because the interface lists all the properties as required but we @@ -1471,7 +1459,7 @@ export class MatrixCall extends TypedEventEmitter { setTimeout(resolve, 200); @@ -1642,9 +1630,9 @@ export class MatrixCall extends TypedEventEmittererror).name == 'UnknownDeviceError') { code = CallErrorCode.UnknownDevices; message = "Unknown devices present in the room"; } - this.emit(CallEvent.Error, new CallError(code, message, error)); + this.emit(CallEvent.Error, new CallError(code, message, error)); this.terminate(CallParty.Local, code, false); // no need to carry on & send the candidate queue, but we also @@ -1734,11 +1722,11 @@ export class MatrixCall extends TypedEventEmitter { - logger.debug( - "call " + this.callId + ": Signalling state changed to: " + - this.peerConn.signalingState, - ); + logger.debug(`call ${this.callId}: Signalling state changed to: ${this.peerConn?.signalingState}`); }; private onTrack = (ev: RTCTrackEvent): void => { @@ -1796,8 +1781,8 @@ export class MatrixCall extends TypedEventEmittere); return; } finally { this.makingOffer = false; @@ -1961,9 +1946,11 @@ export class MatrixCall extends TypedEventEmitter { - const targetProfileInfo = await this.client.getProfileInfo(transferTargetCall.getOpponentMember().userId); - const transfereeProfileInfo = await this.client.getProfileInfo(this.getOpponentMember().userId); + public async transferToCall(transferTargetCall: MatrixCall): Promise { + const targetUserId = transferTargetCall.getOpponentMember()?.userId; + const targetProfileInfo = targetUserId ? await this.client.getProfileInfo(targetUserId) : undefined; + const opponentUserId = this.getOpponentMember()?.userId; + const transfereeProfileInfo = opponentUserId ? await this.client.getProfileInfo(opponentUserId) : undefined; const newCallId = genCallID(); @@ -1972,9 +1959,9 @@ export class MatrixCall extends TypedEventEmittererror)); this.hangup(code, false); return; @@ -2123,7 +2110,7 @@ export class MatrixCall extends TypedEventEmittere); return; } } @@ -2214,12 +2201,12 @@ export class MatrixCall extends TypedEventEmitter { - const bufferedCandidates = this.remoteCandidateBuffer.get(this.opponentPartyId); + const bufferedCandidates = this.remoteCandidateBuffer.get(this.opponentPartyId!); if (bufferedCandidates) { logger.info(`Adding ${bufferedCandidates.length} buffered candidates for opponent ${this.opponentPartyId}`); await this.addIceCandidates(bufferedCandidates); } - this.remoteCandidateBuffer = null; + this.remoteCandidateBuffer.clear(); } private async addIceCandidates(candidates: RTCIceCandidate[]): Promise { @@ -2235,7 +2222,7 @@ export class MatrixCall extends TypedEventEmitter, +): MatrixCall | null { if (!supportsMatrixCall()) return null; const optionsForceTURN = options ? options.forceTURN : false; diff --git a/src/webrtc/callEventHandler.ts b/src/webrtc/callEventHandler.ts index 581a8c1f81c..0fc83231d53 100644 --- a/src/webrtc/callEventHandler.ts +++ b/src/webrtc/callEventHandler.ts @@ -163,7 +163,7 @@ export class CallEventHandler { this.client, event.getRoomId(), { forceTURN: this.client.forceTURN }, - ); + ) ?? undefined; if (!call) { logger.log( "Incoming call ID " + content.call_id + " but this client " + @@ -181,13 +181,13 @@ export class CallEventHandler { // if we stashed candidate events for that call ID, play them back now if (this.candidateEventsByCall.get(call.callId)) { - for (const ev of this.candidateEventsByCall.get(call.callId)) { + for (const ev of this.candidateEventsByCall.get(call.callId)!) { call.onRemoteIceCandidatesReceived(ev); } } // Were we trying to call that user (room)? - let existingCall: MatrixCall; + let existingCall: MatrixCall | undefined; for (const thisCall of this.calls.values()) { const isCalling = [CallState.WaitLocalMedia, CallState.CreateOffer, CallState.InviteSent].includes( thisCall.state, @@ -238,7 +238,7 @@ export class CallEventHandler { if (!this.candidateEventsByCall.has(content.call_id)) { this.candidateEventsByCall.set(content.call_id, []); } - this.candidateEventsByCall.get(content.call_id).push(event); + this.candidateEventsByCall.get(content.call_id)!.push(event); } else { call.onRemoteIceCandidatesReceived(event); } @@ -250,7 +250,7 @@ export class CallEventHandler { // if not live, store the fact that the call has ended because // we're probably getting events backwards so // the hangup will come before the invite - call = createNewMatrixCall(this.client, event.getRoomId()); + call = createNewMatrixCall(this.client, event.getRoomId()) ?? undefined; if (call) { call.callId = content.call_id; call.initWithHangup(event); diff --git a/src/webrtc/callFeed.ts b/src/webrtc/callFeed.ts index a8a20205491..14cf794e86c 100644 --- a/src/webrtc/callFeed.ts +++ b/src/webrtc/callFeed.ts @@ -64,12 +64,12 @@ export class CallFeed extends TypedEventEmitter private audioMuted: boolean; private videoMuted: boolean; private measuringVolumeActivity = false; - private audioContext: AudioContext; - private analyser: AnalyserNode; - private frequencyBinCount: Float32Array; + private audioContext?: AudioContext; + private analyser?: AnalyserNode; + private frequencyBinCount?: Float32Array; private speakingThreshold = SPEAKING_THRESHOLD; private speaking = false; - private volumeLooperTimeout: ReturnType; + private volumeLooperTimeout?: ReturnType; constructor(opts: ICallFeedOpts) { super(); @@ -83,6 +83,7 @@ export class CallFeed extends TypedEventEmitter this.speakingVolumeSamples = new Array(SPEAKING_SAMPLE_COUNT).fill(-Infinity); this.updateStream(null, opts.stream); + this.stream = opts.stream; // updateStream does this, but this makes TS happier if (this.hasAudioTrack) { this.initVolumeMeasuring(); @@ -93,22 +94,21 @@ export class CallFeed extends TypedEventEmitter return this.stream.getAudioTracks().length > 0; } - private updateStream(oldStream: MediaStream, newStream: MediaStream): void { + private updateStream(oldStream: MediaStream | null, newStream: MediaStream): void { if (newStream === oldStream) return; if (oldStream) { oldStream.removeEventListener("addtrack", this.onAddTrack); this.measureVolumeActivity(false); } - if (newStream) { - this.stream = newStream; - newStream.addEventListener("addtrack", this.onAddTrack); - - if (this.hasAudioTrack) { - this.initVolumeMeasuring(); - } else { - this.measureVolumeActivity(false); - } + + this.stream = newStream; + newStream.addEventListener("addtrack", this.onAddTrack); + + if (this.hasAudioTrack) { + this.initVolumeMeasuring(); + } else { + this.measureVolumeActivity(false); } this.emit(CallFeedEvent.NewStream, this.stream); @@ -138,9 +138,9 @@ export class CallFeed extends TypedEventEmitter * Returns callRoom member * @returns member of the callRoom */ - public getMember(): RoomMember { + public getMember(): RoomMember | null { const callRoom = this.client.getRoom(this.roomId); - return callRoom.getMember(this.userId); + return callRoom?.getMember(this.userId) ?? null; } /** @@ -177,9 +177,10 @@ export class CallFeed extends TypedEventEmitter /** * Set one or both of feed's internal audio and video video mute state * Either value may be null to leave it as-is - * @param muted is the feed's video muted? + * @param audioMuted is the feed's audio muted? + * @param videoMuted is the feed's video muted? */ - public setAudioVideoMuted(audioMuted: boolean, videoMuted: boolean): void { + public setAudioVideoMuted(audioMuted: boolean | null, videoMuted: boolean | null): void { if (audioMuted !== null) { if (this.audioMuted !== audioMuted) { this.speakingVolumeSamples.fill(-Infinity); @@ -216,12 +217,12 @@ export class CallFeed extends TypedEventEmitter if (!this.measuringVolumeActivity) return; - this.analyser.getFloatFrequencyData(this.frequencyBinCount); + this.analyser.getFloatFrequencyData(this.frequencyBinCount!); let maxVolume = -Infinity; - for (let i = 0; i < this.frequencyBinCount.length; i++) { - if (this.frequencyBinCount[i] > maxVolume) { - maxVolume = this.frequencyBinCount[i]; + for (let i = 0; i < this.frequencyBinCount!.length; i++) { + if (this.frequencyBinCount![i] > maxVolume) { + maxVolume = this.frequencyBinCount![i]; } } diff --git a/src/webrtc/mediaHandler.ts b/src/webrtc/mediaHandler.ts index ba84ca899a9..a47d6783a01 100644 --- a/src/webrtc/mediaHandler.ts +++ b/src/webrtc/mediaHandler.ts @@ -22,8 +22,8 @@ import { MatrixClient } from "../client"; import { CallState } from "./call"; export class MediaHandler { - private audioInput: string; - private videoInput: string; + private audioInput?: string; + private videoInput?: string; private localUserMediaStream?: MediaStream; public userMediaStreams: MediaStream[] = []; public screensharingStreams: MediaStream[] = []; @@ -75,7 +75,7 @@ export class MediaHandler { for (const call of this.client.callEventHandler.calls.values()) { if (call.state === CallState.Ended || !callMediaStreamParams.has(call.callId)) continue; - const { audio, video } = callMediaStreamParams.get(call.callId); + const { audio, video } = callMediaStreamParams.get(call.callId)!; // This stream won't be reusable as we will replace the tracks of the old stream const stream = await this.getUserMediaStream(audio, video, false); @@ -121,9 +121,9 @@ export class MediaHandler { const settings = track.getSettings(); if (track.kind === "audio") { - this.audioInput = settings.deviceId; + this.audioInput = settings.deviceId!; } else if (track.kind === "video") { - this.videoInput = settings.deviceId; + this.videoInput = settings.deviceId!; } } @@ -179,7 +179,10 @@ export class MediaHandler { * @param reusable is allowed to be reused by the MediaHandler * @returns {MediaStream} based on passed parameters */ - public async getScreensharingStream(desktopCapturerSourceId: string, reusable = true): Promise { + public async getScreensharingStream( + desktopCapturerSourceId?: string, + reusable = true, + ): Promise { let stream: MediaStream; if (this.screensharingStreams.length === 0) {