diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 5f6127278..dfcaf62af 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -64,7 +64,7 @@ jobs: - name: Generate Docs run: "yarn run gendoc" - + - name: Upload Artifact uses: actions/upload-artifact@v3 with: @@ -72,38 +72,3 @@ jobs: path: _docs # We'll only use this in a workflow_run, then we're done with it retention-days: 1 - - tsc-strict: - name: Typescript Strict Error Checker - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - permissions: - pull-requests: read - checks: write - steps: - - uses: actions/checkout@v3 - - - name: Get diff lines - id: diff - uses: Equip-Collaboration/diff-line-numbers@v1.0.0 - with: - include: '["\\.tsx?$"]' - - - name: Detecting files changed - id: files - uses: futuratrepadeira/changed-files@v4.0.0 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - pattern: '^.*\.tsx?$' - - - uses: t3chguy/typescript-check-action@main - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - use-check: false - check-fail-mode: added - output-behaviour: annotate - ts-extra-args: '--noImplicitAny' - files-changed: ${{ steps.files.outputs.files_updated }} - files-added: ${{ steps.files.outputs.files_created }} - files-deleted: ${{ steps.files.outputs.files_deleted }} - line-numbers: ${{ steps.diff.outputs.lineNumbers }} diff --git a/package.json b/package.json index 9ad98499a..76529468d 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types", "build:types": "tsc -p tsconfig-build.json --emitDeclarationOnly", "build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src", - "build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js", + "build:compile-browser": "mkdirp dist && browserify -d src/browser-index.ts -p [ tsify -p ./tsconfig-build.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js", "build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js", "gendoc": "typedoc", "lint": "yarn lint:types && yarn lint:js", @@ -33,9 +33,9 @@ "matrix-org" ], "main": "./src/index.ts", - "browser": "./lib/browser-index.js", + "browser": "./lib/browser-index.ts", "matrix_src_main": "./src/index.ts", - "matrix_src_browser": "./src/browser-index.js", + "matrix_src_browser": "./src/browser-index.ts", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", "author": "matrix.org", @@ -80,7 +80,7 @@ "@babel/preset-typescript": "^7.12.7", "@babel/register": "^7.12.10", "@casualbot/jest-sonar-reporter": "^2.2.5", - "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.13.tgz", + "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz", "@types/bs58": "^4.0.1", "@types/content-type": "^1.1.5", "@types/domexception": "^4.0.0", diff --git a/spec/TestClient.ts b/spec/TestClient.ts index 249f5b39e..02d26b6a7 100644 --- a/spec/TestClient.ts +++ b/spec/TestClient.ts @@ -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! as Record)[userId]).toEqual([]); }); return response; }); diff --git a/spec/browserify/setupTests.ts b/spec/browserify/setupTests.ts index a92a70e23..789c0218d 100644 --- a/spec/browserify/setupTests.ts +++ b/spec/browserify/setupTests.ts @@ -15,19 +15,7 @@ limitations under the License. */ import "../../dist/browser-matrix"; // uses browser-matrix instead of the src -import type { MatrixClient, ClientEvent } from "../../src"; - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace NodeJS { - interface Global { - matrixcs: { - MatrixClient: typeof MatrixClient; - ClientEvent: typeof ClientEvent; - }; - } - } -} +import type { default as BrowserMatrix } from "../../src/browser-index"; // stub for browser-matrix browserify tests // @ts-ignore @@ -43,4 +31,4 @@ afterAll(() => { global.matrixcs = { ...global.matrixcs, timeoutSignal: () => new AbortController().signal, -}; +} as typeof BrowserMatrix; diff --git a/spec/browserify/sync-browserify.spec.ts b/spec/browserify/sync-browserify.spec.ts index 172cb0c47..6898c3e1c 100644 --- a/spec/browserify/sync-browserify.spec.ts +++ b/spec/browserify/sync-browserify.spec.ts @@ -16,7 +16,7 @@ limitations under the License. import HttpBackend from "matrix-mock-request"; -import "./setupTests";// uses browser-matrix instead of the src +import "./setupTests"; // uses browser-matrix instead of the src import type { MatrixClient } from "../../src"; const USER_ID = "@user:test.server"; @@ -65,15 +65,16 @@ describe("Browserify Test", function() { const syncData = { next_batch: "batch1", rooms: { - join: {}, - }, - }; - syncData.rooms.join[ROOM_ID] = { - timeline: { - events: [ - event, - ], - limited: false, + join: { + [ROOM_ID]: { + timeline: { + events: [ + event, + ], + limited: false, + }, + }, + }, }, }; diff --git a/spec/integ/devicelist-integ.spec.ts b/spec/integ/devicelist-integ.spec.ts index acd8f9c80..5b980149b 100644 --- a/spec/integ/devicelist-integ.spec.ts +++ b/spec/integ/devicelist-integ.spec.ts @@ -30,7 +30,7 @@ const ROOM_ID = "!room:id"; * * @return {object} sync response */ -function getSyncResponse(roomMembers) { +function getSyncResponse(roomMembers: string[]) { const stateEvents = [ testUtils.mkEvent({ type: 'm.room.encryption', @@ -43,12 +43,10 @@ function getSyncResponse(roomMembers) { Array.prototype.push.apply( stateEvents, - roomMembers.map( - (m) => testUtils.mkMembership({ - mship: 'join', - sender: m, - }), - ), + roomMembers.map((m) => testUtils.mkMembership({ + mship: 'join', + sender: m, + })), ); const syncResponse = { @@ -73,8 +71,8 @@ describe("DeviceList management:", function() { return; } - let sessionStoreBackend; - let aliceTestClient; + let aliceTestClient: TestClient; + let sessionStoreBackend: Storage; async function createTestClient() { const testClient = new TestClient( @@ -97,7 +95,10 @@ describe("DeviceList management:", function() { }); it("Alice shouldn't do a second /query for non-e2e-capable devices", function() { - aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } }); + aliceTestClient.expectKeyQuery({ + device_keys: { '@alice:localhost': {} }, + failures: {}, + }); return aliceTestClient.start().then(function() { const syncResponse = getSyncResponse(['@bob:xyz']); aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse); @@ -138,7 +139,10 @@ describe("DeviceList management:", function() { it.skip("We should not get confused by out-of-order device query responses", () => { // https://github.com/vector-im/element-web/issues/3126 - aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } }); + aliceTestClient.expectKeyQuery({ + device_keys: { '@alice:localhost': {} }, + failures: {}, + }); return aliceTestClient.start().then(() => { aliceTestClient.httpBackend.when('GET', '/sync').respond( 200, getSyncResponse(['@bob:xyz', '@chris:abc'])); @@ -164,11 +168,12 @@ describe("DeviceList management:", function() { aliceTestClient.httpBackend.flush('/keys/query', 1).then( () => aliceTestClient.httpBackend.flush('/send/', 1), ), - aliceTestClient.client.crypto.deviceList.saveIfDirty(), + aliceTestClient.client.crypto!.deviceList.saveIfDirty(), ]); }).then(() => { - aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - expect(data.syncToken).toEqual(1); + // @ts-ignore accessing a protected field + aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { + expect(data!.syncToken).toEqual(1); }); // invalidate bob's and chris's device lists in separate syncs @@ -201,15 +206,16 @@ describe("DeviceList management:", function() { return aliceTestClient.httpBackend.flush('/keys/query', 1); }).then((flushed) => { expect(flushed).toEqual(0); - return aliceTestClient.client.crypto.deviceList.saveIfDirty(); + return aliceTestClient.client.crypto!.deviceList.saveIfDirty(); }).then(() => { - aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - const bobStat = data.trackingStatus['@bob:xyz']; + // @ts-ignore accessing a protected field + aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { + const bobStat = data!.trackingStatus['@bob:xyz']; if (bobStat != 1 && bobStat != 2) { throw new Error('Unexpected status for bob: wanted 1 or 2, got ' + bobStat); } - const chrisStat = data.trackingStatus['@chris:abc']; + const chrisStat = data!.trackingStatus['@chris:abc']; if (chrisStat != 1 && chrisStat != 2) { throw new Error( 'Unexpected status for chris: wanted 1 or 2, got ' + chrisStat, @@ -234,12 +240,13 @@ describe("DeviceList management:", function() { // wait for the client to stop processing the response return aliceTestClient.client.downloadKeys(['@bob:xyz']); }).then(() => { - return aliceTestClient.client.crypto.deviceList.saveIfDirty(); + return aliceTestClient.client.crypto!.deviceList.saveIfDirty(); }).then(() => { - aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - const bobStat = data.trackingStatus['@bob:xyz']; + // @ts-ignore accessing a protected field + aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { + const bobStat = data!.trackingStatus['@bob:xyz']; expect(bobStat).toEqual(3); - const chrisStat = data.trackingStatus['@chris:abc']; + const chrisStat = data!.trackingStatus['@chris:abc']; if (chrisStat != 1 && chrisStat != 2) { throw new Error( 'Unexpected status for chris: wanted 1 or 2, got ' + bobStat, @@ -255,15 +262,16 @@ describe("DeviceList management:", function() { // wait for the client to stop processing the response return aliceTestClient.client.downloadKeys(['@chris:abc']); }).then(() => { - return aliceTestClient.client.crypto.deviceList.saveIfDirty(); + return aliceTestClient.client.crypto!.deviceList.saveIfDirty(); }).then(() => { - aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - const bobStat = data.trackingStatus['@bob:xyz']; - const chrisStat = data.trackingStatus['@bob:xyz']; + // @ts-ignore accessing a protected field + aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { + const bobStat = data!.trackingStatus['@bob:xyz']; + const chrisStat = data!.trackingStatus['@bob:xyz']; expect(bobStat).toEqual(3); expect(chrisStat).toEqual(3); - expect(data.syncToken).toEqual(3); + expect(data!.syncToken).toEqual(3); }); }); }); @@ -285,10 +293,11 @@ describe("DeviceList management:", function() { }, ); await aliceTestClient.httpBackend.flush('/keys/query', 1); - await aliceTestClient.client.crypto.deviceList.saveIfDirty(); + await aliceTestClient.client.crypto!.deviceList.saveIfDirty(); - aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - const bobStat = data.trackingStatus['@bob:xyz']; + // @ts-ignore accessing a protected field + aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { + const bobStat = data!.trackingStatus['@bob:xyz']; // Alice should be tracking bob's device list expect(bobStat).toBeGreaterThan( @@ -322,10 +331,11 @@ describe("DeviceList management:", function() { ); await aliceTestClient.flushSync(); - await aliceTestClient.client.crypto.deviceList.saveIfDirty(); + await aliceTestClient.client.crypto!.deviceList.saveIfDirty(); - aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - const bobStat = data.trackingStatus['@bob:xyz']; + // @ts-ignore accessing a protected field + aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { + const bobStat = data!.trackingStatus['@bob:xyz']; // Alice should have marked bob's device list as untracked expect(bobStat).toEqual( @@ -359,15 +369,14 @@ describe("DeviceList management:", function() { ); await aliceTestClient.flushSync(); - await aliceTestClient.client.crypto.deviceList.saveIfDirty(); + await aliceTestClient.client.crypto!.deviceList.saveIfDirty(); - aliceTestClient.client.cryptoStore.getEndToEndDeviceData(null, (data) => { - const bobStat = data.trackingStatus['@bob:xyz']; + // @ts-ignore accessing a protected field + aliceTestClient.client.cryptoStore!.getEndToEndDeviceData(null, (data) => { + const bobStat = data!.trackingStatus['@bob:xyz']; // Alice should have marked bob's device list as untracked - expect(bobStat).toEqual( - 0, - ); + expect(bobStat).toEqual(0); }); }); @@ -388,9 +397,7 @@ describe("DeviceList management:", function() { const bobStat = data!.trackingStatus['@bob:xyz']; // Alice should have marked bob's device list as untracked - expect(bobStat).toEqual( - 0, - ); + expect(bobStat).toEqual(0); }); } finally { anotherTestClient.stop(); diff --git a/spec/integ/matrix-client-crypto.spec.ts b/spec/integ/matrix-client-crypto.spec.ts index 847bb8528..adb7dd25b 100644 --- a/spec/integ/matrix-client-crypto.spec.ts +++ b/spec/integ/matrix-client-crypto.spec.ts @@ -28,12 +28,14 @@ limitations under the License. // load olm before the sdk if possible import '../olm-loader'; +import type { Session } from "@matrix-org/olm"; import { logger } from '../../src/logger'; import * as testUtils from "../test-utils/test-utils"; import { TestClient } from "../TestClient"; -import { CRYPTO_ENABLED, IUploadKeysRequest } from "../../src/client"; +import { CRYPTO_ENABLED, IClaimKeysRequest, IQueryKeysRequest, IUploadKeysRequest } from "../../src/client"; import { ClientEvent, IContent, ISendEventResponse, MatrixClient, MatrixEvent } from "../../src/matrix"; import { DeviceInfo } from '../../src/crypto/deviceinfo'; +import { IDeviceKeys, IOneTimeKey } from "../../src/crypto/dehydration"; let aliTestClient: TestClient; const roomId = "!room:localhost"; @@ -47,11 +49,7 @@ const bobAccessToken = "fewgfkuesa"; let aliMessages: IContent[]; let bobMessages: IContent[]; -// IMessage isn't exported by src/crypto/algorithms/olm.ts -interface OlmPayload { - type: number; - body: string; -} +type OlmPayload = ReturnType; async function bobUploadsDeviceKeys(): Promise { bobTestClient.expectDeviceKeyUpload(); @@ -71,12 +69,12 @@ function expectQueryKeys(querier: TestClient, uploader: TestClient): Promise = {}; + uploaderKeys[uploader.deviceId!] = uploader.deviceKeys!; querier.httpBackend.when("POST", "/keys/query") - .respond(200, function(_path, content: IUploadKeysRequest) { + .respond(200, function(_path, content: IQueryKeysRequest) { expect(content.device_keys![uploader.userId!]).toEqual([]); - const result = {}; + const result: Record> = {}; result[uploader.userId!] = uploaderKeys; return { device_keys: result }; }); @@ -94,7 +92,7 @@ async function expectAliClaimKeys(): Promise { const keys = await bobTestClient.awaitOneTimeKeyUpload(); aliTestClient.httpBackend.when( "POST", "/keys/claim", - ).respond(200, function(_path, content: IUploadKeysRequest) { + ).respond(200, function(_path, content: IClaimKeysRequest) { const claimType = content.one_time_keys![bobUserId][bobDeviceId]; expect(claimType).toEqual("signed_curve25519"); let keyId = ''; @@ -105,7 +103,7 @@ async function expectAliClaimKeys(): Promise { } } } - const result = {}; + const result: Record>> = {}; result[bobUserId] = {}; result[bobUserId][bobDeviceId] = {}; result[bobUserId][bobDeviceId][keyId] = keys[keyId]; @@ -276,22 +274,21 @@ async function recvMessage( next_batch: "x", rooms: { join: { - + [roomId]: { + timeline: { + events: [ + testUtils.mkEvent({ + type: "m.room.encrypted", + room: roomId, + content: message, + sender: sender, + }), + ], + }, + }, }, }, }; - syncData.rooms.join[roomId] = { - timeline: { - events: [ - testUtils.mkEvent({ - type: "m.room.encrypted", - room: roomId, - content: message, - sender: sender, - }), - ], - }, - }; httpBackend.when("GET", "/sync").respond(200, syncData); const eventPromise = new Promise((resolve) => { @@ -335,24 +332,25 @@ function firstSync(testClient: TestClient): Promise { const syncData = { next_batch: "x", rooms: { - join: { }, - }, - }; - syncData.rooms.join[roomId] = { - state: { - events: [ - testUtils.mkMembership({ - mship: "join", - user: aliUserId, - }), - testUtils.mkMembership({ - mship: "join", - user: bobUserId, - }), - ], - }, - timeline: { - events: [], + join: { + [roomId]: { + state: { + events: [ + testUtils.mkMembership({ + mship: "join", + user: aliUserId, + }), + testUtils.mkMembership({ + mship: "join", + user: bobUserId, + }), + ], + }, + timeline: { + events: [], + }, + }, + }, }, }; @@ -424,7 +422,7 @@ describe("MatrixClient crypto", () => { }, }; - const bobKeys = {}; + const bobKeys: Record = {}; bobKeys[bobDeviceId] = bobDeviceKeys; aliTestClient.httpBackend.when( "POST", "/keys/query", @@ -460,7 +458,7 @@ describe("MatrixClient crypto", () => { }, }; - const bobKeys = {}; + const bobKeys: Record = {}; bobKeys[bobDeviceId] = bobDeviceKeys; aliTestClient.httpBackend.when( "POST", "/keys/query", @@ -515,22 +513,21 @@ describe("MatrixClient crypto", () => { next_batch: "x", rooms: { join: { - + [roomId]: { + timeline: { + events: [ + testUtils.mkEvent({ + type: "m.room.encrypted", + room: roomId, + content: message, + sender: "@bogus:sender", + }), + ], + }, + }, }, }, }; - syncData.rooms.join[roomId] = { - timeline: { - events: [ - testUtils.mkEvent({ - type: "m.room.encrypted", - room: roomId, - content: message, - sender: "@bogus:sender", - }), - ], - }, - }; bobTestClient.httpBackend.when("GET", "/sync").respond(200, syncData); const eventPromise = new Promise((resolve) => { @@ -607,20 +604,21 @@ describe("MatrixClient crypto", () => { const syncData = { next_batch: '2', rooms: { - join: {}, - }, - }; - syncData.rooms.join[roomId] = { - state: { - events: [ - testUtils.mkEvent({ - type: 'm.room.encryption', - skey: '', - content: { - algorithm: 'm.olm.v1.curve25519-aes-sha2', + join: { + [roomId]: { + state: { + events: [ + testUtils.mkEvent({ + type: 'm.room.encryption', + skey: '', + content: { + algorithm: 'm.olm.v1.curve25519-aes-sha2', + }, + }), + ], }, - }), - ], + }, + }, }, }; diff --git a/spec/integ/matrix-client-event-timeline.spec.ts b/spec/integ/matrix-client-event-timeline.spec.ts index 663af8fbf..5bbb82759 100644 --- a/spec/integ/matrix-client-event-timeline.spec.ts +++ b/spec/integ/matrix-client-event-timeline.spec.ts @@ -536,20 +536,20 @@ describe("MatrixClient event timelines", function() { }; }); - let tl0; - let tl3; + let tl0: EventTimeline; + let tl3: EventTimeline; return Promise.all([ client.getEventTimeline(timelineSet, EVENTS[0].event_id!, ).then(function(tl) { expect(tl!.getEvents().length).toEqual(1); - tl0 = tl; + tl0 = tl!; return client.getEventTimeline(timelineSet, EVENTS[2].event_id!); }).then(function(tl) { expect(tl!.getEvents().length).toEqual(1); return client.getEventTimeline(timelineSet, EVENTS[3].event_id!); }).then(function(tl) { expect(tl!.getEvents().length).toEqual(1); - tl3 = tl; + tl3 = tl!; return client.getEventTimeline(timelineSet, EVENTS[1].event_id!); }).then(function(tl) { // we expect it to get merged in with event 2 @@ -953,11 +953,11 @@ describe("MatrixClient event timelines", function() { }; }); - let tl; + let tl: EventTimeline; return Promise.all([ client.getEventTimeline(timelineSet, EVENTS[0].event_id!, ).then(function(tl0) { - tl = tl0; + tl = tl0!; return client.paginateEventTimeline(tl, { backwards: true }); }).then(function(success) { expect(success).toBeTruthy(); @@ -1043,11 +1043,11 @@ describe("MatrixClient event timelines", function() { }; }); - let tl; + let tl: EventTimeline; return Promise.all([ client.getEventTimeline(timelineSet, EVENTS[0].event_id!, ).then(function(tl0) { - tl = tl0; + tl = tl0!; return client.paginateEventTimeline( tl, { backwards: false, limit: 20 }); }).then(function(success) { @@ -1569,16 +1569,17 @@ describe("MatrixClient event timelines", function() { const syncData = { next_batch: "batch1", rooms: { - join: {}, - }, - }; - syncData.rooms.join[roomId] = { - timeline: { - events: [ - event, - redaction, - ], - limited: false, + join: { + [roomId]: { + timeline: { + events: [ + event, + redaction, + ], + limited: false, + }, + }, + }, }, }; httpBackend.when("GET", "/sync").respond(200, syncData); @@ -1595,18 +1596,19 @@ describe("MatrixClient event timelines", function() { const sync2 = { next_batch: "batch2", rooms: { - join: {}, - }, - }; - sync2.rooms.join[roomId] = { - timeline: { - events: [ - utils.mkMessage({ - user: otherUserId, msg: "world", - }), - ], - limited: true, - prev_batch: "newerTok", + join: { + [roomId]: { + timeline: { + events: [ + utils.mkMessage({ + user: otherUserId, msg: "world", + }), + ], + limited: true, + prev_batch: "newerTok", + }, + }, + }, }, }; httpBackend.when("GET", "/sync").respond(200, sync2); diff --git a/spec/integ/matrix-client-methods.spec.ts b/spec/integ/matrix-client-methods.spec.ts index f2ff9e6df..93c3d3537 100644 --- a/spec/integ/matrix-client-methods.spec.ts +++ b/spec/integ/matrix-client-methods.spec.ts @@ -618,13 +618,13 @@ describe("MatrixClient", function() { }); describe("partitionThreadedEvents", function() { - let room; + let room: Room; beforeEach(() => { room = new Room("!STrMRsukXHtqQdSeHa:matrix.org", client!, userId); }); it("returns empty arrays when given an empty arrays", function() { - const events = []; + const events: MatrixEvent[] = []; const [timeline, threaded] = room.partitionThreadedEvents(events); expect(timeline).toEqual([]); expect(threaded).toEqual([]); @@ -1645,7 +1645,7 @@ const buildEventCreate = () => new MatrixEvent({ "user_id": "@andybalaam-test1:matrix.org", }); -function assertObjectContains(obj: object, expected: any): void { +function assertObjectContains(obj: Record, expected: any): void { for (const k in expected) { if (expected.hasOwnProperty(k)) { expect(obj[k]).toEqual(expected[k]); diff --git a/spec/integ/matrix-client-opts.spec.ts b/spec/integ/matrix-client-opts.spec.ts index 5ea4fba77..41532ea52 100644 --- a/spec/integ/matrix-client-opts.spec.ts +++ b/spec/integ/matrix-client-opts.spec.ts @@ -1,7 +1,7 @@ import HttpBackend from "matrix-mock-request"; import * as utils from "../test-utils/test-utils"; -import { MatrixClient } from "../../src/matrix"; +import { ClientEvent, MatrixClient } from "../../src/matrix"; import { MatrixScheduler } from "../../src/scheduler"; import { MemoryStore } from "../../src/store/memory"; import { MatrixError } from "../../src/http-api"; @@ -65,7 +65,7 @@ describe("MatrixClient opts", function() { }); describe("without opts.store", function() { - let client; + let client: MatrixClient; beforeEach(function() { client = new MatrixClient({ fetchFn: httpBackend.fetchFn as typeof global.fetch, @@ -98,7 +98,7 @@ describe("MatrixClient opts", function() { "m.room.message", "m.room.name", "m.room.member", "m.room.member", "m.room.create", ]; - client.on("event", function(event) { + client.on(ClientEvent.Event, function(event) { expect(expectedEventTypes.indexOf(event.getType())).not.toEqual( -1, ); @@ -125,7 +125,7 @@ describe("MatrixClient opts", function() { }); describe("without opts.scheduler", function() { - let client; + let client: MatrixClient; beforeEach(function() { client = new MatrixClient({ fetchFn: httpBackend.fetchFn as typeof global.fetch, diff --git a/spec/integ/matrix-client-room-timeline.spec.ts b/spec/integ/matrix-client-room-timeline.spec.ts index f89ab04e2..3c2aa77a6 100644 --- a/spec/integ/matrix-client-room-timeline.spec.ts +++ b/spec/integ/matrix-client-room-timeline.spec.ts @@ -18,7 +18,16 @@ import HttpBackend from "matrix-mock-request"; import * as utils from "../test-utils/test-utils"; import { EventStatus } from "../../src/models/event"; -import { MatrixError, ClientEvent, IEvent, MatrixClient, RoomEvent } from "../../src"; +import { + MatrixError, + ClientEvent, + IEvent, + MatrixClient, + RoomEvent, + ISyncResponse, + IMinimalEvent, + IRoomEvent, Room, +} from "../../src"; import { TestClient } from "../TestClient"; describe("MatrixClient room timelines", function() { @@ -39,7 +48,7 @@ describe("MatrixClient room timelines", function() { name: "Old room name", }, }); - let NEXT_SYNC_DATA; + let NEXT_SYNC_DATA: Partial; const SYNC_DATA = { next_batch: "s_5_3", rooms: { @@ -88,7 +97,7 @@ describe("MatrixClient room timelines", function() { }, }, leave: {}, - }, + } as unknown as ISyncResponse["rooms"], }; events.forEach(function(e) { if (e.room_id !== roomId) { @@ -96,11 +105,11 @@ describe("MatrixClient room timelines", function() { } if (e.state_key) { // push the current - NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e); + NEXT_SYNC_DATA.rooms!.join[roomId].timeline.events.push(e as unknown as IRoomEvent); } else if (["m.typing", "m.receipt"].indexOf(e.type!) !== -1) { - NEXT_SYNC_DATA.rooms.join[roomId].ephemeral.events.push(e); + NEXT_SYNC_DATA.rooms!.join[roomId].ephemeral.events.push(e as unknown as IMinimalEvent); } else { - NEXT_SYNC_DATA.rooms.join[roomId].timeline.events.push(e); + NEXT_SYNC_DATA.rooms!.join[roomId].timeline.events.push(e as unknown as IRoomEvent); } }); } @@ -237,7 +246,7 @@ describe("MatrixClient room timelines", function() { }); describe("paginated events", function() { - let sbEvents; + let sbEvents: Partial[]; const sbEndTok = "pagin_end"; beforeEach(function() { @@ -559,7 +568,7 @@ describe("MatrixClient room timelines", function() { utils.mkMessage({ user: userId, room: roomId }), ]; setNextSyncData(eventData); - NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true; + NEXT_SYNC_DATA.rooms!.join[roomId].timeline.limited = true; return Promise.all([ httpBackend!.flush("/versions", 1), @@ -593,7 +602,7 @@ describe("MatrixClient room timelines", function() { utils.mkMessage({ user: userId, room: roomId }), ]; setNextSyncData(eventData); - NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true; + NEXT_SYNC_DATA.rooms!.join[roomId].timeline.limited = true; return Promise.all([ httpBackend!.flush("/sync", 1), @@ -638,7 +647,7 @@ describe("MatrixClient room timelines", function() { end: "end_token", }; - let room; + let room: Room; beforeEach(async () => { setNextSyncData(initialSyncEventData); diff --git a/spec/integ/matrix-client-syncing.spec.ts b/spec/integ/matrix-client-syncing.spec.ts index 8c3969bee..dbb891449 100644 --- a/spec/integ/matrix-client-syncing.spec.ts +++ b/spec/integ/matrix-client-syncing.spec.ts @@ -33,7 +33,7 @@ import { IJoinedRoom, IStateEvent, IMinimalEvent, - NotificationCountType, + NotificationCountType, IEphemeral, } from "../../src"; import { UNREAD_THREAD_NOTIFICATIONS } from '../../src/@types/sync'; import * as utils from "../test-utils/test-utils"; @@ -524,105 +524,101 @@ describe("MatrixClient syncing", () => { const syncData = { rooms: { join: { - + [roomOne]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "hello", + }), + ], + }, + state: { + events: [ + utils.mkEvent({ + type: "m.room.name", room: roomOne, user: otherUserId, + content: { + name: "Old room name", + }, + }), + utils.mkMembership({ + room: roomOne, mship: "join", user: otherUserId, + }), + utils.mkMembership({ + room: roomOne, mship: "join", user: selfUserId, + }), + utils.mkEvent({ + type: "m.room.create", room: roomOne, user: selfUserId, + content: { + creator: selfUserId, + }, + }), + ], + }, + }, + [roomTwo]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomTwo, user: otherUserId, msg: "hiii", + }), + ], + }, + state: { + events: [ + utils.mkMembership({ + room: roomTwo, mship: "join", user: otherUserId, + name: otherDisplayName, + }), + utils.mkMembership({ + room: roomTwo, mship: "join", user: selfUserId, + }), + utils.mkEvent({ + type: "m.room.create", room: roomTwo, user: selfUserId, + content: { + creator: selfUserId, + }, + }), + ], + }, + }, }, }, }; - syncData.rooms.join[roomOne] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomOne, user: otherUserId, msg: "hello", - }), - ], - }, - state: { - events: [ - utils.mkEvent({ - type: "m.room.name", room: roomOne, user: otherUserId, - content: { - name: "Old room name", - }, - }), - utils.mkMembership({ - room: roomOne, mship: "join", user: otherUserId, - }), - utils.mkMembership({ - room: roomOne, mship: "join", user: selfUserId, - }), - utils.mkEvent({ - type: "m.room.create", room: roomOne, user: selfUserId, - content: { - creator: selfUserId, - }, - }), - ], - }, - }; - syncData.rooms.join[roomTwo] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomTwo, user: otherUserId, msg: "hiii", - }), - ], - }, - state: { - events: [ - utils.mkMembership({ - room: roomTwo, mship: "join", user: otherUserId, - name: otherDisplayName, - }), - utils.mkMembership({ - room: roomTwo, mship: "join", user: selfUserId, - }), - utils.mkEvent({ - type: "m.room.create", room: roomTwo, user: selfUserId, - content: { - creator: selfUserId, - }, - }), - ], - }, - }; const nextSyncData = { rooms: { join: { - + [roomOne]: { + state: { + events: [ + utils.mkEvent({ + type: "m.room.name", room: roomOne, user: selfUserId, + content: { name: "A new room name" }, + }), + ], + }, + }, + [roomTwo]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomTwo, user: otherUserId, msg: msgText, + }), + ], + }, + ephemeral: { + events: [ + utils.mkEvent({ + type: "m.typing", room: roomTwo, + content: { user_ids: [otherUserId] }, + }), + ], + }, + }, }, }, }; - nextSyncData.rooms.join[roomOne] = { - state: { - events: [ - utils.mkEvent({ - type: "m.room.name", room: roomOne, user: selfUserId, - content: { name: "A new room name" }, - }), - ], - }, - }; - - nextSyncData.rooms.join[roomTwo] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomTwo, user: otherUserId, msg: msgText, - }), - ], - }, - ephemeral: { - events: [ - utils.mkEvent({ - type: "m.typing", room: roomTwo, - content: { user_ids: [otherUserId] }, - }), - ], - }, - }; - it("should continually recalculate the right room name.", () => { httpBackend!.when("GET", "/sync").respond(200, syncData); httpBackend!.when("GET", "/sync").respond(200, nextSyncData); @@ -635,9 +631,7 @@ describe("MatrixClient syncing", () => { ]).then(() => { const room = client!.getRoom(roomOne)!; // should have clobbered the name to the one from /events - expect(room.name).toEqual( - nextSyncData.rooms.join[roomOne].state.events[0].content.name, - ); + expect(room.name).toEqual(nextSyncData.rooms.join[roomOne].state.events[0].content?.name); }); }); @@ -742,46 +736,48 @@ describe("MatrixClient syncing", () => { const normalFirstSync = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - normalFirstSync.rooms.join[roomOne] = { - timeline: { - events: [normalMessageEvent], - prev_batch: "pagTok", - }, - state: { - events: [roomCreateEvent], + join: { + [roomOne]: { + timeline: { + events: [normalMessageEvent], + prev_batch: "pagTok", + }, + state: { + events: [roomCreateEvent], + }, + }, + }, }, }; const nextSyncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - nextSyncData.rooms.join[roomOne] = { - timeline: { - events: [ - // In subsequent syncs, a marker event in timeline - // range should normally trigger - // `timelineNeedsRefresh=true` but this marker isn't - // being sent by the room creator so it has no - // special meaning in existing room versions. - utils.mkEvent({ - type: UNSTABLE_MSC2716_MARKER.name, - room: roomOne, - // The important part we're testing is here! - // `userC` is not the room creator. - user: userC, - skey: "", - content: { - "m.insertion_id": "$abc", + join: { + [roomOne]: { + timeline: { + events: [ + // In subsequent syncs, a marker event in timeline + // range should normally trigger + // `timelineNeedsRefresh=true` but this marker isn't + // being sent by the room creator so it has no + // special meaning in existing room versions. + utils.mkEvent({ + type: UNSTABLE_MSC2716_MARKER.name, + room: roomOne, + // The important part we're testing is here! + // `userC` is not the room creator. + user: userC, + skey: "", + content: { + "m.insertion_id": "$abc", + }, + }), + ], + prev_batch: "pagTok", }, - }), - ], - prev_batch: "pagTok", + }, + }, }, }; @@ -831,16 +827,17 @@ describe("MatrixClient syncing", () => { const normalFirstSync = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - normalFirstSync.rooms.join[roomOne] = { - timeline: { - events: [normalMessageEvent], - prev_batch: "pagTok", - }, - state: { - events: [roomCreateEvent], + join: { + [roomOne]: { + timeline: { + events: [normalMessageEvent], + prev_batch: "pagTok", + }, + state: { + events: [roomCreateEvent], + }, + }, + }, }, }; @@ -849,16 +846,17 @@ describe("MatrixClient syncing", () => { const syncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - syncData.rooms.join[roomOne] = { - timeline: { - events: [normalMessageEvent], - prev_batch: "pagTok", - }, - state: { - events: [roomCreateEvent], + join: { + [roomOne]: { + timeline: { + events: [normalMessageEvent], + prev_batch: "pagTok", + }, + state: { + events: [roomCreateEvent], + }, + }, + }, }, }; @@ -879,16 +877,17 @@ describe("MatrixClient syncing", () => { const syncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - syncData.rooms.join[roomOne] = { - timeline: { - events: [markerEventFromRoomCreator], - prev_batch: "pagTok", - }, - state: { - events: [roomCreateEvent], + join: { + [roomOne]: { + timeline: { + events: [markerEventFromRoomCreator], + prev_batch: "pagTok", + }, + state: { + events: [roomCreateEvent], + }, + }, + }, }, }; @@ -909,19 +908,20 @@ describe("MatrixClient syncing", () => { const syncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - syncData.rooms.join[roomOne] = { - timeline: { - events: [normalMessageEvent], - prev_batch: "pagTok", - }, - state: { - events: [ - roomCreateEvent, - markerEventFromRoomCreator, - ], + join: { + [roomOne]: { + timeline: { + events: [normalMessageEvent], + prev_batch: "pagTok", + }, + state: { + events: [ + roomCreateEvent, + markerEventFromRoomCreator, + ], + }, + }, + }, }, }; @@ -942,17 +942,18 @@ describe("MatrixClient syncing", () => { const nextSyncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - nextSyncData.rooms.join[roomOne] = { - timeline: { - events: [ - // In subsequent syncs, a marker event in timeline - // range should trigger `timelineNeedsRefresh=true` - markerEventFromRoomCreator, - ], - prev_batch: "pagTok", + join: { + [roomOne]: { + timeline: { + events: [ + // In subsequent syncs, a marker event in timeline + // range should trigger `timelineNeedsRefresh=true` + markerEventFromRoomCreator, + ], + prev_batch: "pagTok", + }, + }, + }, }, }; @@ -993,24 +994,25 @@ describe("MatrixClient syncing", () => { const nextSyncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - nextSyncData.rooms.join[roomOne] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomOne, user: otherUserId, msg: "hello again", - }), - ], - prev_batch: "pagTok", - }, - state: { - events: [ - // In subsequent syncs, a marker event in state - // should trigger `timelineNeedsRefresh=true` - markerEventFromRoomCreator, - ], + join: { + [roomOne]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "hello again", + }), + ], + prev_batch: "pagTok", + }, + state: { + events: [ + // In subsequent syncs, a marker event in state + // should trigger `timelineNeedsRefresh=true` + markerEventFromRoomCreator, + ], + }, + }, + }, }, }; @@ -1095,19 +1097,20 @@ describe("MatrixClient syncing", () => { const limitedSyncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - limitedSyncData.rooms.join[roomOne] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomOne, user: otherUserId, msg: "world", - }), - ], - // The important part, make the sync `limited` - limited: true, - prev_batch: "newerTok", + join: { + [roomOne]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "world", + }), + ], + // The important part, make the sync `limited` + limited: true, + prev_batch: "newerTok", + }, + }, + }, }, }; httpBackend!.when("GET", "/sync").respond(200, limitedSyncData); @@ -1167,7 +1170,7 @@ describe("MatrixClient syncing", () => { const eventsInRoom = syncData.rooms.join[roomOne].timeline.events; const contextUrl = `/rooms/${encodeURIComponent(roomOne)}/context/` + - `${encodeURIComponent(eventsInRoom[0].event_id)}`; + `${encodeURIComponent(eventsInRoom[0].event_id!)}`; httpBackend!.when("GET", contextUrl) .respond(200, () => { return { @@ -1202,17 +1205,18 @@ describe("MatrixClient syncing", () => { const syncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - syncData.rooms.join[roomOne] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomOne, user: otherUserId, msg: "hello", - }), - ], - prev_batch: "pagTok", + join: { + [roomOne]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "hello", + }), + ], + prev_batch: "pagTok", + }, + }, + }, }, }; @@ -1229,17 +1233,18 @@ describe("MatrixClient syncing", () => { const syncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - syncData.rooms.join[roomTwo] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomTwo, user: otherUserId, msg: "roomtwo", - }), - ], - prev_batch: "roomtwotok", + join: { + [roomTwo]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomTwo, user: otherUserId, msg: "roomtwo", + }), + ], + prev_batch: "roomtwotok", + }, + }, + }, }, }; @@ -1261,18 +1266,19 @@ describe("MatrixClient syncing", () => { const syncData = { next_batch: "batch_token", rooms: { - join: {}, - }, - }; - syncData.rooms.join[roomOne] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomOne, user: otherUserId, msg: "world", - }), - ], - limited: true, - prev_batch: "newerTok", + join: { + [roomOne]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "world", + }), + ], + limited: true, + prev_batch: "newerTok", + }, + }, + }, }, }; httpBackend!.when("GET", "/sync").respond(200, syncData); @@ -1304,44 +1310,46 @@ describe("MatrixClient syncing", () => { const syncData = { rooms: { join: { - + [roomOne]: { + ephemeral: { + events: [], + } as IEphemeral, + timeline: { + events: [ + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "hello", + }), + utils.mkMessage({ + room: roomOne, user: otherUserId, msg: "world", + }), + ], + }, + state: { + events: [ + utils.mkEvent({ + type: "m.room.name", room: roomOne, user: otherUserId, + content: { + name: "Old room name", + }, + }), + utils.mkMembership({ + room: roomOne, mship: "join", user: otherUserId, + }), + utils.mkMembership({ + room: roomOne, mship: "join", user: selfUserId, + }), + utils.mkEvent({ + type: "m.room.create", room: roomOne, user: selfUserId, + content: { + creator: selfUserId, + }, + }), + ], + } as Partial, + }, }, }, }; - syncData.rooms.join[roomOne] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomOne, user: otherUserId, msg: "hello", - }), - utils.mkMessage({ - room: roomOne, user: otherUserId, msg: "world", - }), - ], - }, - state: { - events: [ - utils.mkEvent({ - type: "m.room.name", room: roomOne, user: otherUserId, - content: { - name: "Old room name", - }, - }), - utils.mkMembership({ - room: roomOne, mship: "join", user: otherUserId, - }), - utils.mkMembership({ - room: roomOne, mship: "join", user: selfUserId, - }), - utils.mkEvent({ - type: "m.room.create", room: roomOne, user: selfUserId, - content: { - creator: selfUserId, - }, - }), - ], - }, - }; beforeEach(() => { syncData.rooms.join[roomOne].ephemeral = { @@ -1351,16 +1359,15 @@ describe("MatrixClient syncing", () => { it("should sync receipts from /sync.", () => { const ackEvent = syncData.rooms.join[roomOne].timeline.events[0]; - const receipt = {}; - receipt[ackEvent.event_id] = { + const receipt: Record = {}; + receipt[ackEvent.event_id!] = { "m.read": {}, }; - receipt[ackEvent.event_id]["m.read"][userC] = { + receipt[ackEvent.event_id!]["m.read"][userC] = { ts: 176592842636, }; syncData.rooms.join[roomOne].ephemeral.events = [{ content: receipt, - room_id: roomOne, type: "m.receipt", }]; httpBackend!.when("GET", "/sync").respond(200, syncData); @@ -1425,7 +1432,7 @@ describe("MatrixClient syncing", () => { }, }, }, - }; + } as unknown as ISyncResponse; it("should sync unread notifications.", () => { syncData.rooms.join[roomOne][UNREAD_THREAD_NOTIFICATIONS.name] = { [THREAD_ID]: { @@ -1509,18 +1516,18 @@ describe("MatrixClient syncing", () => { const syncData = { next_batch: "batch_token", rooms: { - leave: {}, - }, - }; - - syncData.rooms.leave[roomTwo] = { - timeline: { - events: [ - utils.mkMessage({ - room: roomTwo, user: otherUserId, msg: "hello", - }), - ], - prev_batch: "pagTok", + leave: { + [roomTwo]: { + timeline: { + events: [ + utils.mkMessage({ + room: roomTwo, user: otherUserId, msg: "hello", + }), + ], + prev_batch: "pagTok", + }, + }, + }, }, }; diff --git a/spec/integ/megolm-backup.spec.ts b/spec/integ/megolm-backup.spec.ts index 492e4f1dc..9341d6511 100644 --- a/spec/integ/megolm-backup.spec.ts +++ b/spec/integ/megolm-backup.spec.ts @@ -126,12 +126,13 @@ describe("megolm key backups", function() { const syncResponse = { next_batch: 1, rooms: { - join: {}, - }, - }; - syncResponse.rooms.join[ROOM_ID] = { - timeline: { - events: [ENCRYPTED_EVENT], + join: { + [ROOM_ID]: { + timeline: { + events: [ENCRYPTED_EVENT], + }, + }, + }, }, }; diff --git a/spec/integ/sliding-sync-sdk.spec.ts b/spec/integ/sliding-sync-sdk.spec.ts index 7a37f572b..20dfa6141 100644 --- a/spec/integ/sliding-sync-sdk.spec.ts +++ b/spec/integ/sliding-sync-sdk.spec.ts @@ -119,13 +119,13 @@ describe("SlidingSyncSdk", () => { }; // find an extension on a SlidingSyncSdk instance - const findExtension = (name: string): Extension => { + const findExtension = (name: string): Extension => { expect(mockSlidingSync!.registerExtension).toHaveBeenCalled(); const mockFn = mockSlidingSync!.registerExtension as jest.Mock; // find the extension for (let i = 0; i < mockFn.mock.calls.length; i++) { - const calledExtension = mockFn.mock.calls[i][0] as Extension; - if (calledExtension && calledExtension.name() === name) { + const calledExtension = mockFn.mock.calls[i][0] as Extension; + if (calledExtension?.name() === name) { return calledExtension; } } @@ -581,7 +581,7 @@ describe("SlidingSyncSdk", () => { }); describe("ExtensionE2EE", () => { - let ext: Extension; + let ext: Extension; beforeAll(async () => { await setupClient({ @@ -647,7 +647,7 @@ describe("SlidingSyncSdk", () => { }); describe("ExtensionAccountData", () => { - let ext: Extension; + let ext: Extension; beforeAll(async () => { await setupClient(); @@ -773,7 +773,7 @@ describe("SlidingSyncSdk", () => { }); describe("ExtensionToDevice", () => { - let ext: Extension; + let ext: Extension; beforeAll(async () => { await setupClient(); @@ -871,7 +871,7 @@ describe("SlidingSyncSdk", () => { }); describe("ExtensionTyping", () => { - let ext: Extension; + let ext: Extension; beforeAll(async () => { await setupClient(); @@ -970,7 +970,7 @@ describe("SlidingSyncSdk", () => { }); describe("ExtensionReceipts", () => { - let ext: Extension; + let ext: Extension; const generateReceiptResponse = ( userId: string, roomId: string, eventId: string, recType: string, ts: number, diff --git a/spec/integ/sliding-sync.spec.ts b/spec/integ/sliding-sync.spec.ts index 0b1e9fedd..c2a4b8e98 100644 --- a/spec/integ/sliding-sync.spec.ts +++ b/spec/integ/sliding-sync.spec.ts @@ -18,7 +18,15 @@ limitations under the License. import EventEmitter from "events"; import MockHttpBackend from "matrix-mock-request"; -import { SlidingSync, SlidingSyncState, ExtensionState, SlidingSyncEvent } from "../../src/sliding-sync"; +import { + SlidingSync, + SlidingSyncState, + ExtensionState, + SlidingSyncEvent, + Extension, + SlidingSyncEventHandlerMap, + MSC3575RoomData, +} from "../../src/sliding-sync"; import { TestClient } from "../TestClient"; import { logger } from "../../src/logger"; import { MatrixClient } from "../../src"; @@ -94,7 +102,7 @@ describe("SlidingSync", () => { is_dm: true, }, }; - const ext = { + const ext: Extension = { name: () => "custom_extension", onRequest: (initial) => { return { initial: initial }; }, onResponse: (res) => { return {}; }, @@ -107,7 +115,7 @@ describe("SlidingSync", () => { slidingSync.start(); // expect everything to be sent - let txnId; + let txnId: string | undefined; httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); @@ -390,8 +398,8 @@ describe("SlidingSync", () => { }], rooms: rooms, }); - const listenerData = {}; - const dataListener = (roomId, roomData) => { + const listenerData: Record = {}; + const dataListener: SlidingSyncEventHandlerMap[SlidingSyncEvent.RoomData] = (roomId, roomData) => { expect(listenerData[roomId]).toBeFalsy(); listenerData[roomId] = roomData; }; @@ -912,7 +920,7 @@ describe("SlidingSync", () => { slidingSync = new SlidingSync(proxyBaseUrl, [], roomSubInfo, client!, 1); // modification before SlidingSync.start() const subscribePromise = slidingSync.modifyRoomSubscriptions(new Set([roomId])); - let txnId; + let txnId: string | undefined; httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); @@ -944,7 +952,7 @@ describe("SlidingSync", () => { ranges: [[0, 20]], }; const promise = slidingSync.setList(0, newList); - let txnId; + let txnId: string | undefined; httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); @@ -966,7 +974,7 @@ describe("SlidingSync", () => { }); it("should resolve setListRanges during a connection", async () => { const promise = slidingSync.setListRanges(0, [[20, 40]]); - let txnId; + let txnId: string | undefined; httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); @@ -992,7 +1000,7 @@ describe("SlidingSync", () => { const promise = slidingSync.modifyRoomSubscriptionInfo({ timeline_limit: 99, }); - let txnId; + let txnId: string | undefined; httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); @@ -1016,7 +1024,7 @@ describe("SlidingSync", () => { it("should reject earlier pending promises if a later transaction is acknowledged", async () => { // i.e if we have [A,B,C] and see txn_id=C then A,B should be rejected. const gotTxnIds: any[] = []; - const pushTxn = function(req) { + const pushTxn = function(req: MockHttpBackend["requests"][0]) { gotTxnIds.push(req.data.txn_id); }; const failPromise = slidingSync.setListRanges(0, [[20, 40]]); @@ -1032,7 +1040,7 @@ describe("SlidingSync", () => { expect(failPromise2).rejects.toEqual(gotTxnIds[1]); const okPromise = slidingSync.setListRanges(0, [[0, 20]]); - let txnId; + let txnId: string | undefined; httpBackend!.when("POST", syncUrl).check((req) => { txnId = req.data.txn_id; }).respond(200, () => { @@ -1050,7 +1058,7 @@ describe("SlidingSync", () => { it("should not reject later pending promises if an earlier transaction is acknowledged", async () => { // i.e if we have [A,B,C] and see txn_id=B then C should not be rejected but A should. const gotTxnIds: any[] = []; - const pushTxn = function(req) { + const pushTxn = function(req: MockHttpBackend["requests"][0]) { gotTxnIds.push(req.data?.txn_id); }; const A = slidingSync.setListRanges(0, [[20, 40]]); @@ -1087,7 +1095,7 @@ describe("SlidingSync", () => { promise.finally(() => { pending = false; }); - let txnId; + let txnId: string | undefined; httpBackend!.when("POST", syncUrl).check(function(req) { const body = req.data; logger.debug("got ", body); @@ -1275,21 +1283,21 @@ describe("SlidingSync", () => { // Pre-extensions get called BEFORE processing the sync response const preExtName = "foobar"; - let onPreExtensionRequest; - let onPreExtensionResponse; + let onPreExtensionRequest: Extension["onRequest"]; + let onPreExtensionResponse: Extension["onResponse"]; // Post-extensions get called AFTER processing the sync response const postExtName = "foobar2"; - let onPostExtensionRequest; - let onPostExtensionResponse; + let onPostExtensionRequest: Extension["onRequest"]; + let onPostExtensionResponse: Extension["onResponse"]; - const extPre = { + const extPre: Extension = { name: () => preExtName, onRequest: (initial) => { return onPreExtensionRequest(initial); }, onResponse: (res) => { return onPreExtensionResponse(res); }, when: () => ExtensionState.PreProcess, }; - const extPost = { + const extPost: Extension = { name: () => postExtName, onRequest: (initial) => { return onPostExtensionRequest(initial); }, onResponse: (res) => { return onPostExtensionResponse(res); }, @@ -1421,7 +1429,7 @@ describe("SlidingSync", () => { }); function timeout(delayMs: number, reason: string): { promise: Promise, cancel: () => void } { - let timeoutId; + let timeoutId: NodeJS.Timeout; return { promise: new Promise((resolve, reject) => { timeoutId = setTimeout(() => { @@ -1454,7 +1462,7 @@ function listenUntil( const trace = new Error().stack?.split(`\n`)[2]; const t = timeout(timeoutMs, "timed out waiting for event " + eventName + " " + trace); return Promise.race([new Promise((resolve, reject) => { - const wrapper = (...args) => { + const wrapper = (...args: any[]) => { try { const data = callback(...args); if (data) { diff --git a/spec/test-utils/test-utils.ts b/spec/test-utils/test-utils.ts index 21ae4d18b..3dd9c0378 100644 --- a/spec/test-utils/test-utils.ts +++ b/spec/test-utils/test-utils.ts @@ -332,7 +332,7 @@ export function mkReplyMessage( * * @constructor */ -export class MockStorageApi { +export class MockStorageApi implements Storage { private data: Record = {}; public get length() { @@ -354,6 +354,10 @@ export class MockStorageApi { public removeItem(k: string): void { delete this.data[k]; } + + public clear(): void { + this.data = {}; + } } /** diff --git a/spec/unit/crypto.spec.ts b/spec/unit/crypto.spec.ts index b319a5f26..a4e1ec374 100644 --- a/spec/unit/crypto.spec.ts +++ b/spec/unit/crypto.spec.ts @@ -3,7 +3,7 @@ import '../olm-loader'; import { EventEmitter } from "events"; import type { PkDecryption, PkSigning } from "@matrix-org/olm"; -import { MatrixClient } from "../../src/client"; +import { IClaimOTKsResult, MatrixClient } from "../../src/client"; import { Crypto } from "../../src/crypto"; import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store"; import { MockStorageApi } from "../MockStorageApi"; @@ -23,16 +23,16 @@ import { IRoomEncryption, RoomList } from "../../src/crypto/RoomList"; const Olm = global.Olm; -function awaitEvent(emitter, event) { - return new Promise((resolve, reject) => { +function awaitEvent(emitter: EventEmitter, event: string): Promise { + return new Promise((resolve) => { emitter.once(event, (result) => { resolve(result); }); }); } -async function keyshareEventForEvent(client, event, index): Promise { - const roomId = event.getRoomId(); +async function keyshareEventForEvent(client: MatrixClient, event: MatrixEvent, index?: number): Promise { + const roomId = event.getRoomId()!; const eventContent = event.getWireContent(); const key = await client.crypto!.olmDevice.getInboundGroupSessionKey( roomId, @@ -42,16 +42,16 @@ async function keyshareEventForEvent(client, event, index): Promise ); const ksEvent = new MatrixEvent({ type: "m.forwarded_room_key", - sender: client.getUserId(), + sender: client.getUserId()!, content: { "algorithm": olmlib.MEGOLM_ALGORITHM, "room_id": roomId, "sender_key": eventContent.sender_key, - "sender_claimed_ed25519_key": key.sender_claimed_ed25519_key, + "sender_claimed_ed25519_key": key?.sender_claimed_ed25519_key, "session_id": eventContent.session_id, - "session_key": key.key, - "chain_index": key.chain_index, - "forwarding_curve25519_key_chain": key.forwarding_curve_key_chain, + "session_key": key?.key, + "chain_index": key?.chain_index, + "forwarding_curve25519_key_chain": key?.forwarding_curve25519_key_chain, "org.matrix.msc3061.shared_history": true, }, }); @@ -172,7 +172,8 @@ describe("Crypto", function() { }); describe('Session management', function() { - const otkResponse = { + const otkResponse: IClaimOTKsResult = { + failures: {}, one_time_keys: { '@alice:home.server': { aliceDevice: { @@ -188,11 +189,12 @@ describe("Crypto", function() { }, }, }; - let crypto; - let mockBaseApis; - let mockRoomList; - let fakeEmitter; + let crypto: Crypto; + let mockBaseApis: MatrixClient; + let mockRoomList: RoomList; + + let fakeEmitter: EventEmitter; beforeEach(async function() { const mockStorage = new MockStorageApi() as unknown as Storage; @@ -219,8 +221,8 @@ describe("Crypto", function() { sendToDevice: jest.fn(), getKeyBackupVersion: jest.fn(), isGuest: jest.fn(), - }; - mockRoomList = {}; + } as unknown as MatrixClient; + mockRoomList = {} as unknown as RoomList; fakeEmitter = new EventEmitter(); @@ -233,7 +235,7 @@ describe("Crypto", function() { mockRoomList, [], ); - crypto.registerEventHandlers(fakeEmitter); + crypto.registerEventHandlers(fakeEmitter as any); await crypto.init(); }); @@ -245,7 +247,7 @@ describe("Crypto", function() { const prom = new Promise((resolve) => { mockBaseApis.claimOneTimeKeys = function() { resolve(); - return otkResponse; + return Promise.resolve(otkResponse); }; }); @@ -989,7 +991,7 @@ describe("Crypto", function() { ensureOlmSessionsForDevices.mockResolvedValue({}); encryptMessageForDevice = jest.spyOn(olmlib, "encryptMessageForDevice"); encryptMessageForDevice.mockImplementation(async (...[result,,,,,, payload]) => { - result.plaintext = JSON.stringify(payload); + result.plaintext = { type: 0, body: JSON.stringify(payload) }; }); client = new TestClient("@alice:example.org", "aliceweb"); @@ -998,7 +1000,7 @@ describe("Crypto", function() { encryptedPayload = { algorithm: "m.olm.v1.curve25519-aes-sha2", sender_key: client.client.crypto!.olmDevice.deviceCurve25519Key, - ciphertext: { plaintext: JSON.stringify(payload) }, + ciphertext: { plaintext: { type: 0, body: JSON.stringify(payload) } }, }; }); @@ -1046,7 +1048,7 @@ describe("Crypto", function() { encryptMessageForDevice.mockImplementation(async (...[result,,,, userId, device, payload]) => { // Refuse to encrypt to Carol's desktop device if (userId === "@carol:example.org" && device.deviceId === "caroldesktop") return; - result.plaintext = JSON.stringify(payload); + result.plaintext = { type: 0, body: JSON.stringify(payload) }; }); client.httpBackend diff --git a/spec/unit/crypto/CrossSigningInfo.spec.ts b/spec/unit/crypto/CrossSigningInfo.spec.ts index f6b64cac4..ad02fb217 100644 --- a/spec/unit/crypto/CrossSigningInfo.spec.ts +++ b/spec/unit/crypto/CrossSigningInfo.spec.ts @@ -232,7 +232,7 @@ describe.each([ return store; }], ])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function(name, dbFactory) { - let store; + let store: IndexedDBCryptoStore; beforeAll(() => { store = dbFactory(); diff --git a/spec/unit/crypto/DeviceList.spec.ts b/spec/unit/crypto/DeviceList.spec.ts index 448c92b28..38c0e2df8 100644 --- a/spec/unit/crypto/DeviceList.spec.ts +++ b/spec/unit/crypto/DeviceList.spec.ts @@ -22,6 +22,7 @@ import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store import { DeviceList } from "../../../src/crypto/DeviceList"; import { IDownloadKeyResult, MatrixClient } from "../../../src"; import { OlmDevice } from "../../../src/crypto/OlmDevice"; +import { CryptoStore } from "../../../src/crypto/store/base"; const signedDeviceList: IDownloadKeyResult = { "failures": {}, @@ -88,8 +89,8 @@ const signedDeviceList2: IDownloadKeyResult = { }; describe('DeviceList', function() { - let downloadSpy; - let cryptoStore; + let downloadSpy: jest.Mock; + let cryptoStore: CryptoStore; let deviceLists: DeviceList[] = []; beforeEach(function() { @@ -112,7 +113,7 @@ describe('DeviceList', function() { deviceId: 'HGKAWHRVJQ', } as unknown as MatrixClient; const mockOlm = { - verifySignature: function(key, message, signature) {}, + verifySignature: function(key: string, message: string, signature: string) {}, } as unknown as OlmDevice; const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize); deviceLists.push(dl); diff --git a/spec/unit/crypto/algorithms/megolm.spec.ts b/spec/unit/crypto/algorithms/megolm.spec.ts index b63120199..48bdbc4ad 100644 --- a/spec/unit/crypto/algorithms/megolm.spec.ts +++ b/spec/unit/crypto/algorithms/megolm.spec.ts @@ -17,6 +17,7 @@ limitations under the License. import { mocked, MockedObject } from 'jest-mock'; import '../../../olm-loader'; +import type { OutboundGroupSession } from "@matrix-org/olm"; import * as algorithms from "../../../../src/crypto/algorithms"; import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store"; import * as testUtils from "../../../test-utils/test-utils"; @@ -31,6 +32,7 @@ import { TypedEventEmitter } from '../../../../src/models/typed-event-emitter'; import { ClientEvent, MatrixClient, RoomMember } from '../../../../src'; import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo'; import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning'; +import { MegolmEncryption as MegolmEncryptionClass } from "../../../../src/crypto/algorithms/megolm"; const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!; const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2')!; @@ -87,7 +89,7 @@ describe("MegolmDecryption", function() { }); describe('receives some keys:', function() { - let groupSession; + let groupSession: OutboundGroupSession; beforeEach(async function() { groupSession = new global.Olm.OutboundGroupSession(); groupSession.create(); @@ -298,10 +300,10 @@ describe("MegolmDecryption", function() { describe("session reuse and key reshares", () => { const rotationPeriodMs = 999 * 24 * 60 * 60 * 1000; // 999 days, so we don't have to deal with it - let megolmEncryption; - let aliceDeviceInfo; - let mockRoom; - let olmDevice; + let megolmEncryption: MegolmEncryptionClass; + let aliceDeviceInfo: DeviceInfo; + let mockRoom: Room; + let olmDevice: OlmDevice; beforeEach(async () => { // @ts-ignore assigning to readonly prop @@ -342,7 +344,7 @@ describe("MegolmDecryption", function() { 'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE', ), getFingerprint: jest.fn().mockReturnValue(''), - }; + } as unknown as DeviceInfo; mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({ '@alice:home.server': { @@ -365,7 +367,7 @@ describe("MegolmDecryption", function() { algorithm: 'm.megolm.v1.aes-sha2', rotation_period_ms: rotationPeriodMs, }, - }); + }) as MegolmEncryptionClass; // Splice the real method onto the mock object as megolm uses this method // on the crypto class in order to encrypt / start sessions @@ -381,7 +383,7 @@ describe("MegolmDecryption", function() { [{ userId: "@alice:home.server" }], ), getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false), - }; + } as unknown as Room; }); it("should use larger otkTimeout when preparing to encrypt room", async () => { @@ -397,11 +399,14 @@ describe("MegolmDecryption", function() { }); it("should generate a new session if this one needs rotation", async () => { + // @ts-ignore - private method access const session = await megolmEncryption.prepareNewSession(false); session.creationTime -= rotationPeriodMs + 10000; // a smidge over the rotation time // Inject expired session which needs rotation + // @ts-ignore - private field access megolmEncryption.setupPromise = Promise.resolve(session); + // @ts-ignore - private method access const prepareNewSessionSpy = jest.spyOn(megolmEncryption, "prepareNewSession"); await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", { body: "Some text", @@ -446,8 +451,8 @@ describe("MegolmDecryption", function() { }); mockBaseApis.sendToDevice.mockClear(); - await megolmEncryption.reshareKeyWithDevice( - olmDevice.deviceCurve25519Key, + await megolmEncryption.reshareKeyWithDevice!( + olmDevice.deviceCurve25519Key!, ct1.session_id, '@alice:home.server', aliceDeviceInfo, @@ -466,8 +471,8 @@ describe("MegolmDecryption", function() { ); mockBaseApis.queueToDevice.mockClear(); - await megolmEncryption.reshareKeyWithDevice( - olmDevice.deviceCurve25519Key, + await megolmEncryption.reshareKeyWithDevice!( + olmDevice.deviceCurve25519Key!, ct1.session_id, '@alice:home.server', aliceDeviceInfo, diff --git a/spec/unit/crypto/algorithms/olm.spec.ts b/spec/unit/crypto/algorithms/olm.spec.ts index b24532091..932fb8016 100644 --- a/spec/unit/crypto/algorithms/olm.spec.ts +++ b/spec/unit/crypto/algorithms/olm.spec.ts @@ -31,17 +31,21 @@ function makeOlmDevice() { return olmDevice; } -async function setupSession(initiator, opponent) { +async function setupSession(initiator: OlmDevice, opponent: OlmDevice) { await opponent.generateOneTimeKeys(1); const keys = await opponent.getOneTimeKeys(); const firstKey = Object.values(keys['curve25519'])[0]; - const sid = await initiator.createOutboundSession( - opponent.deviceCurve25519Key, firstKey, - ); + const sid = await initiator.createOutboundSession(opponent.deviceCurve25519Key!, firstKey); return sid; } +function alwaysSucceed(promise: Promise): Promise { + // swallow any exception thrown by a promise, so that + // Promise.all doesn't abort + return promise.catch(() => {}); +} + describe("OlmDevice", function() { if (!global.Olm) { logger.warn('Not running megolm unit tests: libolm not present'); @@ -159,11 +163,6 @@ describe("OlmDevice", function() { }, "ABCDEFG"), ], }; - function alwaysSucceed(promise) { - // swallow any exception thrown by a promise, so that - // Promise.all doesn't abort - return promise.catch(() => {}); - } // start two tasks that try to ensure that there's an olm session const promises = Promise.all([ @@ -235,12 +234,6 @@ describe("OlmDevice", function() { ], }; - function alwaysSucceed(promise) { - // swallow any exception thrown by a promise, so that - // Promise.all doesn't abort - return promise.catch(() => {}); - } - const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices( aliceOlmDevice, baseApis, devicesByUserAB, )); diff --git a/spec/unit/crypto/backup.spec.ts b/spec/unit/crypto/backup.spec.ts index 0d0820cd3..60fddedc7 100644 --- a/spec/unit/crypto/backup.spec.ts +++ b/spec/unit/crypto/backup.spec.ts @@ -15,8 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MockedObject } from "jest-mock"; - import '../../olm-loader'; import { logger } from "../../../src/logger"; import * as olmlib from "../../../src/crypto/olmlib"; @@ -30,7 +28,10 @@ import { Crypto } from "../../../src/crypto"; import { resetCrossSigningKeys } from "./crypto-utils"; import { BackupManager } from "../../../src/crypto/backup"; import { StubStore } from "../../../src/store/stub"; -import { MatrixScheduler } from '../../../src'; +import { IndexedDBCryptoStore, MatrixScheduler } from '../../../src'; +import { CryptoStore } from "../../../src/crypto/store/base"; +import { MegolmDecryption as MegolmDecryptionClass } from "../../../src/crypto/algorithms/megolm"; +import { IKeyBackupInfo } from "../../../src/crypto/keybackup"; const Olm = global.Olm; @@ -102,29 +103,36 @@ const CURVE25519_BACKUP_INFO = { }, }; -const AES256_BACKUP_INFO = { +const AES256_BACKUP_INFO: IKeyBackupInfo = { algorithm: "org.matrix.msc3270.v1.aes-hmac-sha2", version: '1', - auth_data: { - // FIXME: add iv and mac - }, + auth_data: {} as IKeyBackupInfo["auth_data"], }; -const keys = {}; +const keys: Record = {}; -function getCrossSigningKey(type) { - return keys[type]; +function getCrossSigningKey(type: string) { + return Promise.resolve(keys[type]); } -function saveCrossSigningKeys(k) { +function saveCrossSigningKeys(k: Record) { Object.assign(keys, k); } -function makeTestClient(cryptoStore) { - const scheduler = [ - "getQueueForEvent", "queueEvent", "removeEventFromQueue", +function makeTestScheduler(): MatrixScheduler { + return ([ + "getQueueForEvent", + "queueEvent", + "removeEventFromQueue", "setProcessFunction", - ].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject; + ] as const).reduce((r, k) => { + r[k] = jest.fn(); + return r; + }, {} as MatrixScheduler); +} + +function makeTestClient(cryptoStore: CryptoStore) { + const scheduler = makeTestScheduler(); const store = new StubStore(); return new MatrixClient({ @@ -151,36 +159,33 @@ describe("MegolmBackup", function() { return Olm.init(); }); - let olmDevice; - let mockOlmLib; - let mockCrypto; - let cryptoStore; - let megolmDecryption; + let olmDevice: OlmDevice; + let mockOlmLib: typeof olmlib; + let mockCrypto: Crypto; + let cryptoStore: CryptoStore; + let megolmDecryption: MegolmDecryptionClass; beforeEach(async function() { mockCrypto = testUtils.mock(Crypto, 'Crypto'); + // @ts-ignore making mock mockCrypto.backupManager = testUtils.mock(BackupManager, "BackupManager"); - mockCrypto.backupKey = new Olm.PkEncryption(); - mockCrypto.backupKey.set_recipient_key( - "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo", - ); - mockCrypto.backupInfo = CURVE25519_BACKUP_INFO; + mockCrypto.backupManager.backupInfo = CURVE25519_BACKUP_INFO; cryptoStore = new MemoryCryptoStore(); olmDevice = new OlmDevice(cryptoStore); // we stub out the olm encryption bits - mockOlmLib = {}; + mockOlmLib = {} as unknown as typeof olmlib; mockOlmLib.ensureOlmSessionsForDevices = jest.fn(); mockOlmLib.encryptMessageForDevice = jest.fn().mockResolvedValue(undefined); }); describe("backup", function() { - let mockBaseApis; + let mockBaseApis: MatrixClient; beforeEach(function() { - mockBaseApis = {}; + mockBaseApis = {} as unknown as MatrixClient; megolmDecryption = new MegolmDecryption({ userId: '@user:id', @@ -188,8 +193,9 @@ describe("MegolmBackup", function() { olmDevice: olmDevice, baseApis: mockBaseApis, roomId: ROOM_ID, - }); + }) as MegolmDecryptionClass; + // @ts-ignore private field access megolmDecryption.olmlib = mockOlmLib; // clobber the setTimeout function to run 100x faster. @@ -239,6 +245,7 @@ describe("MegolmBackup", function() { }; mockCrypto.cancelRoomKeyRequest = function() {}; + // @ts-ignore readonly field write mockCrypto.backupManager = { backupGroupSession: jest.fn(), }; @@ -264,21 +271,22 @@ describe("MegolmBackup", function() { olmDevice: olmDevice, baseApis: client, roomId: ROOM_ID, - }); + }) as MegolmDecryptionClass; + // @ts-ignore private field access megolmDecryption.olmlib = mockOlmLib; return client.initCrypto() .then(() => { return cryptoStore.doTxn( "readwrite", - [cryptoStore.STORE_SESSION], + [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { cryptoStore.addEndToEndInboundGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", groupSession.session_id(), { - forwardingCurve25519KeyChain: undefined, + forwardingCurve25519KeyChain: undefined!, keysClaimed: { ed25519: "SENDER_ED25519", }, @@ -298,25 +306,25 @@ describe("MegolmBackup", function() { }); let numCalls = 0; return new Promise((resolve, reject) => { - client.http.authedRequest = function( + client.http.authedRequest = function( method, path, queryParams, data, opts, - ): Promise { + ): any { ++numCalls; expect(numCalls).toBeLessThanOrEqual(1); if (numCalls >= 2) { // exit out of retry loop if there's something wrong reject(new Error("authedRequest called too many timmes")); - return Promise.resolve({} as T); + return Promise.resolve({}); } expect(method).toBe("PUT"); expect(path).toBe("/room_keys/keys"); - expect(queryParams.version).toBe('1'); + expect(queryParams?.version).toBe('1'); expect((data as Record).rooms[ROOM_ID].sessions).toBeDefined(); expect((data as Record).rooms[ROOM_ID].sessions).toHaveProperty( groupSession.session_id(), ); resolve(); - return Promise.resolve({} as T); + return Promise.resolve({}); }; client.crypto!.backupManager.backupGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", @@ -343,8 +351,9 @@ describe("MegolmBackup", function() { olmDevice: olmDevice, baseApis: client, roomId: ROOM_ID, - }); + }) as MegolmDecryptionClass; + // @ts-ignore private field access megolmDecryption.olmlib = mockOlmLib; return client.initCrypto() @@ -354,13 +363,13 @@ describe("MegolmBackup", function() { .then(() => { return cryptoStore.doTxn( "readwrite", - [cryptoStore.STORE_SESSION], + [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { cryptoStore.addEndToEndInboundGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", groupSession.session_id(), { - forwardingCurve25519KeyChain: undefined, + forwardingCurve25519KeyChain: undefined!, keysClaimed: { ed25519: "SENDER_ED25519", }, @@ -381,25 +390,25 @@ describe("MegolmBackup", function() { }); let numCalls = 0; return new Promise((resolve, reject) => { - client.http.authedRequest = function( + client.http.authedRequest = function( method, path, queryParams, data, opts, - ): Promise { + ): any { ++numCalls; expect(numCalls).toBeLessThanOrEqual(1); if (numCalls >= 2) { // exit out of retry loop if there's something wrong reject(new Error("authedRequest called too many timmes")); - return Promise.resolve({} as T); + return Promise.resolve({}); } expect(method).toBe("PUT"); expect(path).toBe("/room_keys/keys"); - expect(queryParams.version).toBe('1'); + expect(queryParams?.version).toBe('1'); expect((data as Record).rooms[ROOM_ID].sessions).toBeDefined(); expect((data as Record).rooms[ROOM_ID].sessions).toHaveProperty( groupSession.session_id(), ); resolve(); - return Promise.resolve({} as T); + return Promise.resolve({}); }; client.crypto!.backupManager.backupGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", @@ -426,8 +435,9 @@ describe("MegolmBackup", function() { olmDevice: olmDevice, baseApis: client, roomId: ROOM_ID, - }); + }) as MegolmDecryptionClass; + // @ts-ignore private field access megolmDecryption.olmlib = mockOlmLib; await client.initCrypto(); @@ -437,10 +447,10 @@ describe("MegolmBackup", function() { let numCalls = 0; await Promise.all([ new Promise((resolve, reject) => { - let backupInfo; + let backupInfo: Record | BodyInit | undefined; client.http.authedRequest = function( method, path, queryParams, data, opts, - ) { + ): any { ++numCalls; expect(numCalls).toBeLessThanOrEqual(2); if (numCalls === 1) { @@ -486,10 +496,7 @@ describe("MegolmBackup", function() { const ibGroupSession = new Olm.InboundGroupSession(); ibGroupSession.create(groupSession.session_key()); - const scheduler = [ - "getQueueForEvent", "queueEvent", "removeEventFromQueue", - "setProcessFunction", - ].reduce((r, k) => {r[k] = jest.fn(); return r;}, {}) as MockedObject; + const scheduler = makeTestScheduler(); const store = new StubStore(); const client = new MatrixClient({ baseUrl: "https://my.home.server", @@ -509,20 +516,21 @@ describe("MegolmBackup", function() { olmDevice: olmDevice, baseApis: client, roomId: ROOM_ID, - }); + }) as MegolmDecryptionClass; + // @ts-ignore private field access megolmDecryption.olmlib = mockOlmLib; await client.initCrypto(); await cryptoStore.doTxn( "readwrite", - [cryptoStore.STORE_SESSION], + [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { cryptoStore.addEndToEndInboundGroupSession( "F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI", groupSession.session_id(), { - forwardingCurve25519KeyChain: undefined, + forwardingCurve25519KeyChain: undefined!, keysClaimed: { ed25519: "SENDER_ED25519", }, @@ -542,26 +550,26 @@ describe("MegolmBackup", function() { let numCalls = 0; await new Promise((resolve, reject) => { - client.http.authedRequest = function( + client.http.authedRequest = function( method, path, queryParams, data, opts, - ): Promise { + ): any { ++numCalls; expect(numCalls).toBeLessThanOrEqual(2); if (numCalls >= 3) { // exit out of retry loop if there's something wrong reject(new Error("authedRequest called too many timmes")); - return Promise.resolve({} as T); + return Promise.resolve({}); } expect(method).toBe("PUT"); expect(path).toBe("/room_keys/keys"); - expect(queryParams.version).toBe('1'); + expect(queryParams?.version).toBe('1'); expect((data as Record).rooms[ROOM_ID].sessions).toBeDefined(); expect((data as Record).rooms[ROOM_ID].sessions).toHaveProperty( groupSession.session_id(), ); if (numCalls > 1) { resolve(); - return Promise.resolve({} as T); + return Promise.resolve({}); } else { return Promise.reject( new Error("this is an expected failure"), @@ -579,7 +587,7 @@ describe("MegolmBackup", function() { }); describe("restore", function() { - let client; + let client: MatrixClient; beforeEach(function() { client = makeTestClient(cryptoStore); @@ -590,8 +598,9 @@ describe("MegolmBackup", function() { olmDevice: olmDevice, baseApis: client, roomId: ROOM_ID, - }); + }) as MegolmDecryptionClass; + // @ts-ignore private field access megolmDecryption.olmlib = mockOlmLib; return client.initCrypto(); @@ -603,7 +612,7 @@ describe("MegolmBackup", function() { it('can restore from backup (Curve25519 version)', function() { client.http.authedRequest = function() { - return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); + return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); }; return client.restoreKeyBackupWithRecoveryKey( "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", @@ -620,7 +629,7 @@ describe("MegolmBackup", function() { it('can restore from backup (AES-256 version)', function() { client.http.authedRequest = function() { - return Promise.resolve(AES256_KEY_BACKUP_DATA); + return Promise.resolve(AES256_KEY_BACKUP_DATA); }; return client.restoreKeyBackupWithRecoveryKey( "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", @@ -637,7 +646,7 @@ describe("MegolmBackup", function() { it('can restore backup by room (Curve25519 version)', function() { client.http.authedRequest = function() { - return Promise.resolve({ + return Promise.resolve({ rooms: { [ROOM_ID]: { sessions: { @@ -649,7 +658,7 @@ describe("MegolmBackup", function() { }; return client.restoreKeyBackupWithRecoveryKey( "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", - null, null, CURVE25519_BACKUP_INFO, + null!, null!, CURVE25519_BACKUP_INFO, ).then(() => { return megolmDecryption.decryptEvent(ENCRYPTED_EVENT); }).then((res) => { @@ -659,18 +668,18 @@ describe("MegolmBackup", function() { it('has working cache functions', async function() { const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]); - await client.crypto.storeSessionBackupPrivateKey(key); - const result = await client.crypto.getSessionBackupPrivateKey(); - expect(new Uint8Array(result)).toEqual(key); + await client.crypto!.storeSessionBackupPrivateKey(key); + const result = await client.crypto!.getSessionBackupPrivateKey(); + expect(new Uint8Array(result!)).toEqual(key); }); it('caches session backup keys as it encounters them', async function() { - const cachedNull = await client.crypto.getSessionBackupPrivateKey(); + const cachedNull = await client.crypto!.getSessionBackupPrivateKey(); expect(cachedNull).toBeNull(); client.http.authedRequest = function() { - return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); + return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); }; - await new Promise((resolve) => { + await new Promise((resolve) => { client.restoreKeyBackupWithRecoveryKey( "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d", ROOM_ID, @@ -679,7 +688,7 @@ describe("MegolmBackup", function() { { cacheCompleteCallback: resolve }, ); }); - const cachedKey = await client.crypto.getSessionBackupPrivateKey(); + const cachedKey = await client.crypto!.getSessionBackupPrivateKey(); expect(cachedKey).not.toBeNull(); }); @@ -688,7 +697,7 @@ describe("MegolmBackup", function() { algorithm: "this.algorithm.does.not.exist", }); client.http.authedRequest = function() { - return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); + return Promise.resolve(CURVE25519_KEY_BACKUP_DATA); }; await expect(client.restoreKeyBackupWithRecoveryKey( @@ -702,10 +711,7 @@ describe("MegolmBackup", function() { 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 scheduler = makeTestScheduler(); const store = new StubStore(); const client = new MatrixClient({ baseUrl: "https://my.home.server", diff --git a/spec/unit/crypto/cross-signing.spec.ts b/spec/unit/crypto/cross-signing.spec.ts index e9c112c50..fe5b37eb2 100644 --- a/spec/unit/crypto/cross-signing.spec.ts +++ b/spec/unit/crypto/cross-signing.spec.ts @@ -18,23 +18,24 @@ limitations under the License. import '../../olm-loader'; import anotherjson from 'another-json'; import { PkSigning } from '@matrix-org/olm'; +import HttpBackend from "matrix-mock-request"; import * as olmlib from "../../../src/crypto/olmlib"; import { MatrixError } from '../../../src/http-api'; import { logger } from '../../../src/logger'; import { ICrossSigningKey, ICreateClientOpts, ISignedKey } from '../../../src/client'; -import { CryptoEvent } from '../../../src/crypto'; +import { CryptoEvent, IBootstrapCrossSigningOpts } from '../../../src/crypto'; import { IDevice } from '../../../src/crypto/deviceinfo'; import { TestClient } from '../../TestClient'; import { resetCrossSigningKeys } from "./crypto-utils"; -const PUSH_RULES_RESPONSE = { +const PUSH_RULES_RESPONSE: Response = { method: "GET", path: "/pushrules/", data: {}, }; -const filterResponse = function(userId) { +const filterResponse = function(userId: string): Response { const filterPath = "/user/" + encodeURIComponent(userId) + "/filter"; return { method: "POST", @@ -43,7 +44,13 @@ const filterResponse = function(userId) { }; }; -function setHttpResponses(httpBackend, responses) { +interface Response { + method: 'GET' | 'PUT' | 'POST' | 'DELETE'; + path: string; + data: object; +} + +function setHttpResponses(httpBackend: HttpBackend, responses: Response[]) { responses.forEach(response => { httpBackend .when(response.method, response.path) @@ -54,13 +61,13 @@ function setHttpResponses(httpBackend, responses) { async function makeTestClient( userInfo: { userId: string, deviceId: string}, options: Partial = {}, - keys = {}, + keys: Record = {}, ) { - function getCrossSigningKey(type) { - return keys[type]; + function getCrossSigningKey(type: string) { + return keys[type] ?? null; } - function saveCrossSigningKeys(k) { + function saveCrossSigningKeys(k: Record) { Object.assign(keys, k); } @@ -142,7 +149,9 @@ describe("Cross Signing", function() { alice.uploadKeySignatures = async () => ({ failures: {} }); alice.setAccountData = async () => ({}); alice.getAccountDataFromServer = async (): Promise => ({} as T); - const authUploadDeviceSigningKeys = async func => await func({}); + const authUploadDeviceSigningKeys: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"] = async func => { + await func({}); + }; // Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass // through failure, stopping before actually applying changes. @@ -275,7 +284,7 @@ describe("Cross Signing", function() { ); // feed sync result that includes master key, ssk, device key - const responses = [ + const responses: Response[] = [ PUSH_RULES_RESPONSE, { method: "POST", @@ -464,7 +473,7 @@ describe("Cross Signing", function() { }); it.skip("should trust signatures received from other devices", async function() { - const aliceKeys: Record = {}; + const aliceKeys: Record = {}; const { client: alice, httpBackend } = await makeTestClient( { userId: "@alice:example.com", deviceId: "Osborne2" }, undefined, @@ -494,8 +503,7 @@ describe("Cross Signing", function() { }); // @ts-ignore private property - const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"] - .Osborne2; + const deviceInfo = alice.crypto!.deviceList.devices["@alice:example.com"].Osborne2; const aliceDevice = { user_id: "@alice:example.com", device_id: "Osborne2", @@ -549,7 +557,7 @@ describe("Cross Signing", function() { // - ssk // - master key signed by her usk (pretend that it was signed by another // of Alice's devices) - const responses = [ + const responses: Response[] = [ PUSH_RULES_RESPONSE, { method: "POST", @@ -853,7 +861,7 @@ describe("Cross Signing", function() { }); it("should offer to upgrade device verifications to cross-signing", async function() { - let upgradeResolveFunc; + let upgradeResolveFunc: Function; const { client: alice } = await makeTestClient( { userId: "@alice:example.com", deviceId: "Osborne2" }, diff --git a/spec/unit/crypto/crypto-utils.ts b/spec/unit/crypto/crypto-utils.ts index 1391d79f1..76f2a0835 100644 --- a/spec/unit/crypto/crypto-utils.ts +++ b/spec/unit/crypto/crypto-utils.ts @@ -1,14 +1,16 @@ import { IRecoveryKey } from '../../../src/crypto/api'; import { CrossSigningLevel } from '../../../src/crypto/CrossSigning'; import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store'; +import { MatrixClient } from "../../../src"; +import { CryptoEvent } from "../../../src/crypto"; // needs to be phased out and replaced with bootstrapSecretStorage, // but that is doing too much extra stuff for it to be an easy transition. export async function resetCrossSigningKeys( - client, + client: MatrixClient, { level }: { level?: CrossSigningLevel} = {}, ): Promise { - const crypto = client.crypto; + const crypto = client.crypto!; const oldKeys = Object.assign({}, crypto.crossSigningInfo.keys); try { @@ -28,7 +30,8 @@ export async function resetCrossSigningKeys( crypto.crossSigningInfo.keys = oldKeys; throw e; } - crypto.emit("crossSigning.keysChanged", {}); + crypto.emit(CryptoEvent.KeysChanged, {}); + // @ts-ignore await crypto.afterCrossSigningLocalKeyChange(); } diff --git a/spec/unit/crypto/secrets.spec.ts b/spec/unit/crypto/secrets.spec.ts index 386df0d22..885f0f6c3 100644 --- a/spec/unit/crypto/secrets.spec.ts +++ b/spec/unit/crypto/secrets.spec.ts @@ -16,6 +16,7 @@ limitations under the License. import '../../olm-loader'; import * as olmlib from "../../../src/crypto/olmlib"; +import { IObject } from "../../../src/crypto/olmlib"; import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/crypto/SecretStorage"; import { MatrixEvent } from "../../../src/models/event"; import { TestClient } from '../../TestClient'; @@ -23,9 +24,11 @@ import { makeTestClients } from './verification/util'; import { encryptAES } from "../../../src/crypto/aes"; import { createSecretStorageKey, resetCrossSigningKeys } from "./crypto-utils"; import { logger } from '../../../src/logger'; -import { ClientEvent, ICreateClientOpts } from '../../../src/client'; +import { ClientEvent, ICreateClientOpts, ICrossSigningKey, MatrixClient } from '../../../src/client'; import { ISecretStorageKeyInfo } from '../../../src/crypto/api'; import { DeviceInfo } from '../../../src/crypto/deviceinfo'; +import { ISignatures } from "../../../src/@types/signed"; +import { ICurve25519AuthData } from "../../../src/crypto/keybackup"; async function makeTestClient(userInfo: { userId: string, deviceId: string}, options: Partial = {}) { const client = (new TestClient( @@ -48,9 +51,15 @@ async function makeTestClient(userInfo: { userId: string, deviceId: string}, opt // Wrapper around pkSign to return a signed object. pkSign returns the // signature, rather than the signed object. -function sign(obj, key, userId) { +function sign(obj: T, key: Uint8Array, userId: string): T & { + signatures: ISignatures; + unsigned?: object; +} { olmlib.pkSign(obj, key, userId, ''); - return obj; + return obj as T & { + signatures: ISignatures; + unsigned?: object; + }; } describe("Secrets", function() { @@ -169,12 +178,12 @@ describe("Secrets", function() { return [newKeyId, key]; }); - let keys = {}; + let keys: Record = {}; const alice = await makeTestClient( { userId: "@alice:example.com", deviceId: "Osborne2" }, { cryptoCallbacks: { - getCrossSigningKey: t => keys[t], + getCrossSigningKey: t => Promise.resolve(keys[t]), saveCrossSigningKeys: k => keys = k, getSecretStorageKey: getKey, }, @@ -227,7 +236,7 @@ describe("Secrets", function() { cryptoCallbacks: { onSecretRequested: (userId, deviceId, requestId, secretName, deviceTrust) => { expect(secretName).toBe("foo"); - return "bar"; + return Promise.resolve("bar"); }, }, }, @@ -354,7 +363,7 @@ describe("Secrets", function() { const storagePublicKey = decryption.generate_key(); const storagePrivateKey = decryption.get_private_key(); - const bob = await makeTestClient( + const bob: MatrixClient = await makeTestClient( { userId: "@bob:example.com", deviceId: "bob1", @@ -364,15 +373,15 @@ describe("Secrets", function() { getSecretStorageKey: async request => { const defaultKeyId = await bob.getDefaultSecretStorageKeyId(); expect(Object.keys(request.keys)).toEqual([defaultKeyId]); - return [defaultKeyId, storagePrivateKey]; + return [defaultKeyId!, storagePrivateKey]; }, }, }, ); - bob.uploadDeviceSigningKeys = async () => {}; - bob.uploadKeySignatures = async () => {}; - bob.setAccountData = async function(eventType, contents, callback) { + bob.uploadDeviceSigningKeys = async () => ({}); + bob.uploadKeySignatures = async () => ({ failures: {} }); + bob.setAccountData = async function(eventType, contents) { const event = new MatrixEvent({ type: eventType, content: contents, @@ -380,16 +389,19 @@ describe("Secrets", function() { this.store.storeAccountDataEvents([ event, ]); - this.emit("accountData", event); + this.emit(ClientEvent.AccountData, event); + return {}; }; - bob.crypto.backupManager.checkKeyBackup = async () => {}; + bob.crypto!.backupManager.checkKeyBackup = async () => null; - const crossSigning = bob.crypto.crossSigningInfo; - const secretStorage = bob.crypto.secretStorage; + const crossSigning = bob.crypto!.crossSigningInfo; + const secretStorage = bob.crypto!.secretStorage; // Set up cross-signing keys from scratch with specific storage key await bob.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: async func => await func({}), + authUploadDeviceSigningKeys: async func => { + await func({}); + }, }); await bob.bootstrapSecretStorage({ createSecretStorageKey: async () => ({ @@ -400,13 +412,15 @@ describe("Secrets", function() { }); // Clear local cross-signing keys and read from secret storage - bob.crypto.deviceList.storeCrossSigningForUser( + bob.crypto!.deviceList.storeCrossSigningForUser( "@bob:example.com", crossSigning.toStorage(), ); crossSigning.keys = {}; await bob.bootstrapCrossSigning({ - authUploadDeviceSigningKeys: async func => await func({}), + authUploadDeviceSigningKeys: async func => { + await func({}); + }, }); expect(crossSigning.getId()).toBeTruthy(); @@ -422,7 +436,7 @@ describe("Secrets", function() { user_signing: USK, self_signing: SSK, }; - const secretStorageKeys = { + const secretStorageKeys: Record = { key_id: SSSSKey, }; const alice = await makeTestClient( @@ -498,14 +512,14 @@ describe("Secrets", function() { [`ed25519:${XSPubKey}`]: XSPubKey, }, }, - self_signing: sign({ + self_signing: sign({ user_id: "@alice:example.com", usage: ["self_signing"], keys: { [`ed25519:${SSPubKey}`]: SSPubKey, }, }, XSK, "@alice:example.com"), - user_signing: sign({ + user_signing: sign({ user_id: "@alice:example.com", usage: ["user_signing"], keys: { @@ -557,7 +571,7 @@ describe("Secrets", function() { user_signing: USK, self_signing: SSK, }; - const secretStorageKeys = { + const secretStorageKeys: Record = { key_id: SSSSKey, }; const alice = await makeTestClient( @@ -642,14 +656,14 @@ describe("Secrets", function() { [`ed25519:${XSPubKey}`]: XSPubKey, }, }, - self_signing: sign({ + self_signing: sign({ user_id: "@alice:example.com", usage: ["self_signing"], keys: { [`ed25519:${SSPubKey}`]: SSPubKey, }, }, XSK, "@alice:example.com"), - user_signing: sign({ + user_signing: sign({ user_id: "@alice:example.com", usage: ["user_signing"], keys: { diff --git a/spec/unit/crypto/verification/sas.spec.ts b/spec/unit/crypto/verification/sas.spec.ts index ee058c7f0..2d05b31b2 100644 --- a/spec/unit/crypto/verification/sas.spec.ts +++ b/spec/unit/crypto/verification/sas.spec.ts @@ -18,12 +18,12 @@ import "../../../olm-loader"; import { makeTestClients } from './util'; import { MatrixEvent } from "../../../../src/models/event"; import { ISasEvent, SAS, SasEvent } from "../../../../src/crypto/verification/SAS"; -import { DeviceInfo } from "../../../../src/crypto/deviceinfo"; +import { DeviceInfo, IDevice } 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 as Verification, VerificationBase } from "../../../../src/crypto/verification/Base"; +import { 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"; @@ -31,8 +31,8 @@ import { TestClient } from "../../../TestClient"; const Olm = global.Olm; -let ALICE_DEVICES; -let BOB_DEVICES; +let ALICE_DEVICES: Record; +let BOB_DEVICES: Record; describe("SAS verification", function() { if (!global.Olm) { @@ -75,7 +75,7 @@ describe("SAS verification", function() { let bob: TestClient; let aliceSasEvent: ISasEvent | null; let bobSasEvent: ISasEvent | null; - let aliceVerifier: Verification; + let aliceVerifier: SAS; let bobPromise: Promise>; let clearTestClientTimeouts: () => void; @@ -95,25 +95,25 @@ describe("SAS verification", function() { ALICE_DEVICES = { Osborne2: { - user_id: "@alice:example.com", - device_id: "Osborne2", algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], keys: { - "ed25519:Osborne2": aliceDevice.deviceEd25519Key, - "curve25519:Osborne2": aliceDevice.deviceCurve25519Key, + "ed25519:Osborne2": aliceDevice.deviceEd25519Key!, + "curve25519:Osborne2": aliceDevice.deviceCurve25519Key!, }, + verified: DeviceInfo.DeviceVerification.UNVERIFIED, + known: false, }, }; BOB_DEVICES = { Dynabook: { - user_id: "@bob:example.com", - device_id: "Dynabook", algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM], keys: { - "ed25519:Dynabook": bobDevice.deviceEd25519Key, - "curve25519:Dynabook": bobDevice.deviceCurve25519Key, + "ed25519:Dynabook": bobDevice.deviceEd25519Key!, + "curve25519:Dynabook": bobDevice.deviceCurve25519Key!, }, + verified: DeviceInfo.DeviceVerification.UNVERIFIED, + known: false, }, }; @@ -136,7 +136,7 @@ describe("SAS verification", function() { bobPromise = new Promise>((resolve, reject) => { bob.client.on(CryptoEvent.VerificationRequest, request => { - request.verifier!.on("show_sas", (e) => { + (request.verifier!).on(SasEvent.ShowSas, (e) => { if (!e.sas.emoji || !e.sas.decimal) { e.cancel(); } else if (!aliceSasEvent) { @@ -158,7 +158,7 @@ describe("SAS verification", function() { aliceVerifier = alice.client.beginKeyVerification( verificationMethods.SAS, bob.client.getUserId()!, bob.deviceId!, - ); + ) as SAS; aliceVerifier.on(SasEvent.ShowSas, (e) => { if (!e.sas.emoji || !e.sas.decimal) { e.cancel(); @@ -413,7 +413,7 @@ 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(SasEvent.ShowSas, (e) => { e.mismatch(); }); resolve(request.verifier!); @@ -443,13 +443,13 @@ describe("SAS verification", function() { }); describe("verification in DM", function() { - 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: SAS; + let bobPromise: Promise; + let clearTestClientTimeouts: Function; beforeEach(async function() { [[alice, bob], clearTestClientTimeouts] = await makeTestClients( @@ -477,7 +477,7 @@ describe("SAS verification", function() { ); }; alice.client.downloadKeys = () => { - return Promise.resolve(); + return Promise.resolve({}); }; bob.client.crypto!.setDeviceVerification = jest.fn(); @@ -495,16 +495,16 @@ describe("SAS verification", function() { return "bob+base64+ed25519+key"; }; bob.client.downloadKeys = () => { - return Promise.resolve(); + return Promise.resolve({}); }; aliceSasEvent = null; bobSasEvent = null; bobPromise = new Promise((resolve, reject) => { - bob.client.on("crypto.verification.request", async (request) => { - const verifier = request.beginKeyVerification(SAS.NAME); - verifier.on("show_sas", (e) => { + bob.client.on(CryptoEvent.VerificationRequest, async (request) => { + const verifier = request.beginKeyVerification(SAS.NAME) as SAS; + verifier.on(SasEvent.ShowSas, (e) => { if (!e.sas.emoji || !e.sas.decimal) { e.cancel(); } else if (!aliceSasEvent) { @@ -525,12 +525,10 @@ describe("SAS verification", function() { }); }); - const aliceRequest = await alice.client.requestVerificationDM( - bob.client.getUserId(), "!room_id", - ); + const aliceRequest = await alice.client.requestVerificationDM(bob.client.getUserId()!, "!room_id"); await aliceRequest.waitFor(r => r.started); - aliceVerifier = aliceRequest.verifier; - aliceVerifier.on("show_sas", (e) => { + aliceVerifier = aliceRequest.verifier! as SAS; + aliceVerifier.on(SasEvent.ShowSas, (e) => { if (!e.sas.emoji || !e.sas.decimal) { e.cancel(); } else if (!bobSasEvent) { diff --git a/spec/unit/crypto/verification/secret_request.spec.ts b/spec/unit/crypto/verification/secret_request.spec.ts index 2f9fafb5d..20ea64d4a 100644 --- a/spec/unit/crypto/verification/secret_request.spec.ts +++ b/spec/unit/crypto/verification/secret_request.spec.ts @@ -125,7 +125,7 @@ describe("self-verifications", () => { expect(restoreKeyBackupWithCache).toHaveBeenCalled(); expect(result).toBeInstanceOf(Array); - expect(result[0][0]).toBe(testKeyPub); - expect(result[1][0]).toBe(testKeyPub); + expect(result![0][0]).toBe(testKeyPub); + expect(result![1][0]).toBe(testKeyPub); }); }); diff --git a/spec/unit/crypto/verification/util.ts b/spec/unit/crypto/verification/util.ts index 5efbe4ed5..f66d6464f 100644 --- a/spec/unit/crypto/verification/util.ts +++ b/spec/unit/crypto/verification/util.ts @@ -16,13 +16,21 @@ limitations under the License. */ import { TestClient } from '../../../TestClient'; -import { MatrixEvent } from "../../../../src/models/event"; +import { IContent, MatrixEvent } from "../../../../src/models/event"; import { IRoomTimelineData } from "../../../../src/models/event-timeline-set"; import { Room, RoomEvent } from "../../../../src/models/room"; import { logger } from '../../../../src/logger'; -import { MatrixClient, ClientEvent } from '../../../../src/client'; +import { MatrixClient, ClientEvent, ICreateClientOpts } from '../../../../src/client'; -export async function makeTestClients(userInfos, options): Promise<[TestClient[], () => void]> { +interface UserInfo { + userId: string; + deviceId: string; +} + +export async function makeTestClients( + userInfos: UserInfo[], + options: Partial, +): Promise<[TestClient[], () => void]> { const clients: TestClient[] = []; const timeouts: ReturnType[] = []; const clientMap: Record> = {}; @@ -51,7 +59,7 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[] } return {}; }; - const makeSendEvent = (matrixClient: MatrixClient) => (room, type, content) => { + const makeSendEvent = (matrixClient: MatrixClient) => (room: string, type: string, content: IContent) => { // make up a unique ID as the event ID const eventId = "$" + matrixClient.makeTxnId(); const rawEvent = { @@ -88,11 +96,12 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[] }; for (const userInfo of userInfos) { - let keys = {}; + let keys: Record = {}; if (!options) options = {}; if (!options.cryptoCallbacks) options.cryptoCallbacks = {}; if (!options.cryptoCallbacks.saveCrossSigningKeys) { options.cryptoCallbacks.saveCrossSigningKeys = k => { keys = k; }; + // @ts-ignore tsc getting confused by overloads options.cryptoCallbacks.getCrossSigningKey = typ => keys[typ]; } const testClient = new TestClient( @@ -104,6 +113,7 @@ export async function makeTestClients(userInfos, options): Promise<[TestClient[] } clientMap[userInfo.userId][userInfo.deviceId] = testClient.client; testClient.client.sendToDevice = makeSendToDevice(testClient.client); + // @ts-ignore tsc getting confused by overloads testClient.client.sendEvent = makeSendEvent(testClient.client); clients.push(testClient); } diff --git a/spec/unit/crypto/verification/verification_request.spec.ts b/spec/unit/crypto/verification/verification_request.spec.ts index 6549c3af7..2dce928e8 100644 --- a/spec/unit/crypto/verification/verification_request.spec.ts +++ b/spec/unit/crypto/verification/verification_request.spec.ts @@ -18,7 +18,7 @@ import { VerificationRequest, READY_TYPE, START_TYPE, DONE_TYPE } from import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel"; import { ToDeviceChannel } from "../../../../src/crypto/verification/request/ToDeviceChannel"; -import { MatrixEvent } from "../../../../src/models/event"; +import { IContent, MatrixEvent } from "../../../../src/models/event"; import { MatrixClient } from "../../../../src/client"; import { IVerificationChannel } from "../../../../src/crypto/verification/request/Channel"; import { VerificationBase } from "../../../../src/crypto/verification/Base"; @@ -30,12 +30,12 @@ type MockClient = MatrixClient & { function makeMockClient(userId: string, deviceId: string): MockClient { let counter = 1; let events: MatrixEvent[] = []; - const deviceEvents = {}; + const deviceEvents: Record> = {}; return { getUserId() { return userId; }, getDeviceId() { return deviceId; }, - sendEvent(roomId, type, content) { + sendEvent(roomId: string, type: string, content: IContent) { counter = counter + 1; const eventId = `$${userId}-${deviceId}-${counter}`; events.push(new MatrixEvent({ @@ -49,7 +49,7 @@ function makeMockClient(userId: string, deviceId: string): MockClient { return Promise.resolve({ event_id: eventId }); }, - sendToDevice(type, msgMap) { + sendToDevice(type: string, msgMap: Record>) { for (const userId of Object.keys(msgMap)) { const deviceMap = msgMap[userId]; for (const deviceId of Object.keys(deviceMap)) { @@ -111,7 +111,7 @@ class MockVerifier extends VerificationBase<'', any> { } } - async handleEvent(event) { + async handleEvent(event: MatrixEvent) { if (event.getType() === DONE_TYPE && !this._startEvent) { await this._channel.send(DONE_TYPE, {}); } @@ -122,7 +122,7 @@ class MockVerifier extends VerificationBase<'', any> { } } -function makeRemoteEcho(event) { +function makeRemoteEcho(event: MatrixEvent) { return new MatrixEvent(Object.assign({}, event.event, { unsigned: { transaction_id: "abc", diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index ba4be9f9b..419ef5020 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -17,7 +17,7 @@ limitations under the License. import { mocked } from "jest-mock"; import { logger } from "../../src/logger"; -import { MatrixClient, ClientEvent } from "../../src/client"; +import { ClientEvent, ITurnServerResponse, MatrixClient, Store } from "../../src/client"; import { Filter } from "../../src/filter"; import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace"; import { @@ -36,7 +36,16 @@ 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, MatrixError, Room } from "../../src"; +import { + ContentHelpers, + EventTimeline, ICreateRoomOpts, + IRequestOpts, + MatrixError, + MatrixHttpApi, + MatrixScheduler, + Method, + Room, +} from "../../src"; import { supportsMatrixCall } from "../../src/webrtc/call"; import { makeBeaconEvent } from "../test-utils/beacon"; import { @@ -44,6 +53,9 @@ import { POLICIES_ACCOUNT_EVENT_TYPE, PolicyScope, } from "../../src/models/invites-ignorer"; +import { IOlmDevice } from "../../src/crypto/algorithms/megolm"; +import { QueryDict } from "../../src/utils"; +import { SyncState } from "../../src/sync"; jest.useFakeTimers(); @@ -52,17 +64,36 @@ jest.mock("../../src/webrtc/call", () => ({ supportsMatrixCall: jest.fn(() => false), })); +type HttpLookup = { + method: string; + path: string; + data?: Record; + error?: object; + expectBody?: Record; + expectQueryParams?: QueryDict; + thenCall?: Function; +}; + +interface Options extends ICreateRoomOpts { + _roomId?: string; +} + +type WrappedRoom = Room & { + _options: Options; + _state: Map; +}; + describe("MatrixClient", function() { const userId = "@alice:bar"; const identityServerUrl = "https://identity.server"; const identityServerDomain = "identity.server"; - let client; - let store; - let scheduler; + let client: MatrixClient; + let store: Store; + let scheduler: MatrixScheduler; const KEEP_ALIVE_PATH = "/_matrix/client/versions"; - const PUSH_RULES_RESPONSE = { + const PUSH_RULES_RESPONSE: HttpLookup = { method: "GET", path: "/pushrules/", data: {}, @@ -70,7 +101,7 @@ describe("MatrixClient", function() { const FILTER_PATH = "/user/" + encodeURIComponent(userId) + "/filter"; - const FILTER_RESPONSE = { + const FILTER_RESPONSE: HttpLookup = { method: "POST", path: FILTER_PATH, data: { filter_id: "f1lt3r" }, @@ -82,29 +113,21 @@ describe("MatrixClient", function() { rooms: {}, }; - const SYNC_RESPONSE = { + const SYNC_RESPONSE: HttpLookup = { method: "GET", path: "/sync", data: SYNC_DATA, }; // 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 httpLookups: HttpLookup[] = []; let acceptKeepalives: boolean; let pendingLookup: { promise: Promise; method: string; path: string; } | null = null; - function httpReq(method, path, qp, data, prefix) { + function httpReq(method: Method, path: string, qp?: QueryDict, data?: BodyInit, opts?: IRequestOpts) { if (path === KEEP_ALIVE_PATH && acceptKeepalives) { return Promise.resolve({ unstable_features: { @@ -145,7 +168,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]); }); } @@ -184,24 +207,38 @@ describe("MatrixClient", function() { userId: userId, }); // FIXME: We shouldn't be yanking http like this. - client.http = [ - "authedRequest", "getContentUri", "request", "uploadContent", - ].reduce((r, k) => { r[k] = jest.fn(); return r; }, {}); - client.http.authedRequest.mockImplementation(httpReq); - client.http.request.mockImplementation(httpReq); + client.http = ([ + "authedRequest", + "getContentUri", + "request", + "uploadContent", + ] as const).reduce((r, k) => { + r[k] = jest.fn(); + return r; + }, {} as MatrixHttpApi); + mocked(client.http.authedRequest).mockImplementation(httpReq); + mocked(client.http.request).mockImplementation(httpReq); } beforeEach(function() { - scheduler = [ - "getQueueForEvent", "queueEvent", "removeEventFromQueue", + scheduler = ([ + "getQueueForEvent", + "queueEvent", + "removeEventFromQueue", "setProcessFunction", - ].reduce((r, k) => { r[k] = jest.fn(); return r; }, {}); - store = [ + ] as const).reduce((r, k) => { + r[k] = jest.fn(); + return r; + }, {} as MatrixScheduler); + store = ([ "getRoom", "getRooms", "getUser", "getSyncToken", "scrollback", "save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter", - "getSyncAccumulator", "startup", "deleteAllData", - ].reduce((r, k) => { r[k] = jest.fn(); return r; }, {}); + "startup", "deleteAllData", + ] as const).reduce((r, k) => { + r[k] = jest.fn(); + return r; + }, {} as Store); store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null)); store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null)); store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null)); @@ -225,7 +262,7 @@ describe("MatrixClient", function() { // means they may call /events and then fail an expect() which will fail // a DIFFERENT test (pollution between tests!) - we return unresolved // promises to stop the client from continuing to run. - client.http.authedRequest.mockImplementation(function() { + mocked(client.http.authedRequest).mockImplementation(function() { return new Promise(() => {}); }); client.stopClient(); @@ -289,7 +326,7 @@ describe("MatrixClient", function() { const txnId = client.makeTxnId(); const room = new Room(roomId, client, userId); - store.getRoom.mockReturnValue(room); + mocked(store.getRoom).mockReturnValue(room); const rootEvent = new MatrixEvent({ event_id: threadId }); room.createThread(threadId, rootEvent, [rootEvent], false); @@ -329,7 +366,7 @@ describe("MatrixClient", function() { }; const room = new Room(roomId, client, userId); - store.getRoom.mockReturnValue(room); + mocked(store.getRoom).mockReturnValue(room); const rootEvent = new MatrixEvent({ event_id: threadId }); room.createThread(threadId, rootEvent, [rootEvent], false); @@ -359,7 +396,7 @@ describe("MatrixClient", function() { const userId = "@test:example.org"; const roomId = "!room:example.org"; const roomName = "Test Tree"; - const mockRoom = {}; + const mockRoom = {} as unknown as Room; const fn = jest.fn().mockImplementation((opts) => { expect(opts).toMatchObject({ name: roomName, @@ -431,23 +468,23 @@ describe("MatrixClient", function() { throw new Error("Unexpected event type or state key"); } }, - }, - }; + } as Room["currentState"], + } as unknown as Room; client.getRoom = (getRoomId) => { expect(getRoomId).toEqual(roomId); return mockRoom; }; const tree = client.unstableGetFileTreeSpace(roomId); expect(tree).toBeDefined(); - expect(tree.roomId).toEqual(roomId); - expect(tree.room).toBe(mockRoom); + expect(tree!.roomId).toEqual(roomId); + expect(tree!.room).toBe(mockRoom); }); it("should not get (unstable) file trees if not joined", async () => { const roomId = "!room:example.org"; const mockRoom = { getMyMembership: () => "leave", // "not join" - }; + } as unknown as Room; client.getRoom = (getRoomId) => { expect(getRoomId).toEqual(roomId); return mockRoom; @@ -491,8 +528,8 @@ describe("MatrixClient", function() { throw new Error("Unexpected event type or state key"); } }, - }, - }; + } as Room["currentState"], + } as unknown as Room; client.getRoom = (getRoomId) => { expect(getRoomId).toEqual(roomId); return mockRoom; @@ -525,8 +562,8 @@ describe("MatrixClient", function() { throw new Error("Unexpected event type or state key"); } }, - }, - }; + } as Room["currentState"], + } as unknown as Room; client.getRoom = (getRoomId) => { expect(getRoomId).toEqual(roomId); return mockRoom; @@ -541,15 +578,15 @@ describe("MatrixClient", function() { SYNC_RESPONSE, ]; const filterId = "ehfewf"; - store.getFilterIdByName.mockReturnValue(filterId); + mocked(store.getFilterIdByName).mockReturnValue(filterId); const filter = new Filter("0", filterId); filter.setDefinition({ "room": { "timeline": { "limit": 8 } } }); - store.getFilter.mockReturnValue(filter); + mocked(store.getFilter).mockReturnValue(filter); const syncPromise = new Promise((resolve, reject) => { - client.on("sync", function syncListener(state) { + client.on(ClientEvent.Sync, function syncListener(state) { if (state === "SYNCING") { expect(httpLookups.length).toEqual(0); - client.removeListener("sync", syncListener); + client.removeListener(ClientEvent.Sync, syncListener); resolve(); } else if (state === "ERROR") { reject(new Error("sync error")); @@ -567,10 +604,10 @@ describe("MatrixClient", function() { it("should return the same sync state as emitted sync events", async function() { const syncingPromise = new Promise((resolve) => { - client.on("sync", function syncListener(state) { + client.on(ClientEvent.Sync, function syncListener(state) { expect(state).toEqual(client.getSyncState()); if (state === "SYNCING") { - client.removeListener("sync", syncListener); + client.removeListener(ClientEvent.Sync, syncListener); resolve(); } }); @@ -586,7 +623,7 @@ describe("MatrixClient", function() { it("should use an existing filter if id is present in localStorage", function() { }); it("should handle localStorage filterId missing from the server", function(done) { - function getFilterName(userId, suffix?: string) { + function getFilterName(userId: string, suffix?: string) { // scope this on the user ID because people may login on many accounts // and they all need to be stored! return "FILTER_SYNC_" + userId + (suffix ? "_" + suffix : ""); @@ -605,14 +642,14 @@ describe("MatrixClient", function() { }, }); httpLookups.push(FILTER_RESPONSE); - store.getFilterIdByName.mockReturnValue(invalidFilterId); + mocked(store.getFilterIdByName).mockReturnValue(invalidFilterId); - const filterName = getFilterName(client.credentials.userId); + const filterName = getFilterName(client.credentials.userId!); client.store.setFilterIdByName(filterName, invalidFilterId); const filter = new Filter(client.credentials.userId); client.getOrCreateFilter(filterName, filter).then(function(filterId) { - expect(filterId).toEqual(FILTER_RESPONSE.data.filter_id); + expect(filterId).toEqual(FILTER_RESPONSE.data?.filter_id); done(); }); }); @@ -634,13 +671,13 @@ describe("MatrixClient", function() { httpLookups.push(FILTER_RESPONSE); httpLookups.push(SYNC_RESPONSE); - client.on("sync", function syncListener(state) { + client.on(ClientEvent.Sync, function syncListener(state) { if (state === "ERROR" && httpLookups.length > 0) { expect(httpLookups.length).toEqual(2); expect(client.retryImmediately()).toBe(true); jest.advanceTimersByTime(1); } else if (state === "PREPARED" && httpLookups.length === 0) { - client.removeListener("sync", syncListener); + client.removeListener(ClientEvent.Sync, syncListener); done(); } else { // unexpected state transition! @@ -658,7 +695,7 @@ describe("MatrixClient", function() { method: "GET", path: "/sync", data: SYNC_DATA, }); - client.on("sync", function syncListener(state) { + client.on(ClientEvent.Sync, function syncListener(state) { if (state === "ERROR" && httpLookups.length > 0) { expect(httpLookups.length).toEqual(1); expect(client.retryImmediately()).toBe( @@ -668,7 +705,7 @@ describe("MatrixClient", function() { } else if (state === "RECONNECTING" && httpLookups.length > 0) { jest.advanceTimersByTime(10000); } else if (state === "SYNCING" && httpLookups.length === 0) { - client.removeListener("sync", syncListener); + client.removeListener(ClientEvent.Sync, syncListener); done(); } }); @@ -684,13 +721,13 @@ describe("MatrixClient", function() { httpLookups.push(FILTER_RESPONSE); httpLookups.push(SYNC_RESPONSE); - client.on("sync", function syncListener(state) { + client.on(ClientEvent.Sync, function syncListener(state) { if (state === "ERROR" && httpLookups.length > 0) { expect(httpLookups.length).toEqual(3); expect(client.retryImmediately()).toBe(true); jest.advanceTimersByTime(1); } else if (state === "PREPARED" && httpLookups.length === 0) { - client.removeListener("sync", syncListener); + client.removeListener(ClientEvent.Sync, syncListener); done(); } else { // unexpected state transition! @@ -702,8 +739,8 @@ describe("MatrixClient", function() { }); describe("emitted sync events", function() { - function syncChecker(expectedStates, done) { - return function syncListener(state, old) { + function syncChecker(expectedStates: [string, string | null][], done: Function) { + return function syncListener(state: SyncState, old: SyncState | null) { const expected = expectedStates.shift(); logger.log( "'sync' curr=%s old=%s EXPECT=%s", state, old, expected, @@ -715,7 +752,7 @@ describe("MatrixClient", function() { expect(state).toEqual(expected[0]); expect(old).toEqual(expected[1]); if (expectedStates.length === 0) { - client.removeListener("sync", syncListener); + client.removeListener(ClientEvent.Sync, syncListener); done(); } // standard retry time is 5 to 10 seconds @@ -726,7 +763,7 @@ describe("MatrixClient", function() { it("should transition null -> PREPARED after the first /sync", function(done) { const expectedStates: [string, string | null][] = []; expectedStates.push(["PREPARED", null]); - client.on("sync", syncChecker(expectedStates, done)); + client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.startClient(); }); @@ -738,7 +775,7 @@ describe("MatrixClient", function() { method: "POST", path: FILTER_PATH, error: { errcode: "NOPE_NOPE_NOPE" }, }); expectedStates.push(["ERROR", null]); - client.on("sync", syncChecker(expectedStates, done)); + client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.startClient(); }); @@ -768,7 +805,7 @@ describe("MatrixClient", function() { expectedStates.push(["RECONNECTING", null]); expectedStates.push(["ERROR", "RECONNECTING"]); expectedStates.push(["CATCHUP", "ERROR"]); - client.on("sync", syncChecker(expectedStates, done)); + client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.startClient(); }); @@ -776,7 +813,7 @@ describe("MatrixClient", function() { const expectedStates: [string, string | null][] = []; expectedStates.push(["PREPARED", null]); expectedStates.push(["SYNCING", "PREPARED"]); - client.on("sync", syncChecker(expectedStates, done)); + client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.startClient(); }); @@ -795,7 +832,7 @@ describe("MatrixClient", function() { expectedStates.push(["SYNCING", "PREPARED"]); expectedStates.push(["RECONNECTING", "SYNCING"]); expectedStates.push(["ERROR", "RECONNECTING"]); - client.on("sync", syncChecker(expectedStates, done)); + client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.startClient(); }); @@ -809,7 +846,7 @@ describe("MatrixClient", function() { expectedStates.push(["PREPARED", null]); expectedStates.push(["SYNCING", "PREPARED"]); expectedStates.push(["ERROR", "SYNCING"]); - client.on("sync", syncChecker(expectedStates, done)); + client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.startClient(); }); @@ -821,7 +858,7 @@ describe("MatrixClient", function() { expectedStates.push(["PREPARED", null]); expectedStates.push(["SYNCING", "PREPARED"]); expectedStates.push(["SYNCING", "SYNCING"]); - client.on("sync", syncChecker(expectedStates, done)); + client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.startClient(); }); @@ -845,7 +882,7 @@ describe("MatrixClient", function() { expectedStates.push(["RECONNECTING", "SYNCING"]); expectedStates.push(["ERROR", "RECONNECTING"]); expectedStates.push(["ERROR", "ERROR"]); - client.on("sync", syncChecker(expectedStates, done)); + client.on(ClientEvent.Sync, syncChecker(expectedStates, done)); client.startClient(); }); }); @@ -914,14 +951,14 @@ describe("MatrixClient", function() { throw new Error("Unexpected event type or state key"); } }, - }, + } as Room["currentState"], getThread: jest.fn(), addPendingEvent: jest.fn(), updatePendingEvent: jest.fn(), reEmitter: { reEmit: jest.fn(), }, - }; + } as unknown as Room; beforeEach(() => { client.getRoom = (getRoomId) => { @@ -987,7 +1024,7 @@ describe("MatrixClient", function() { const mockRoom = { getMyMembership: () => "join", - updatePendingEvent: (event, status) => event.setStatus(status), + updatePendingEvent: (event: MatrixEvent, status: EventStatus) => event.setStatus(status), currentState: { getStateEvents: (eventType, stateKey) => { if (eventType === EventType.RoomCreate) { @@ -1004,15 +1041,14 @@ describe("MatrixClient", function() { throw new Error("Unexpected event type or state key"); } }, - }, - }; + } as Room["currentState"], + } as unknown as Room; - let event; + let event: MatrixEvent; beforeEach(async () => { event = new MatrixEvent({ event_id: "~" + roomId + ":" + txnId, - user_id: client.credentials.userId, - sender: client.credentials.userId, + sender: client.credentials.userId!, room_id: roomId, origin_server_ts: new Date().getTime(), }); @@ -1023,25 +1059,26 @@ describe("MatrixClient", function() { return mockRoom; }; client.crypto = { // mock crypto - encryptEvent: (event, room) => new Promise(() => {}), + encryptEvent: () => new Promise(() => {}), stop: jest.fn(), - }; + } as unknown as Crypto; }); function assertCancelled() { expect(event.status).toBe(EventStatus.CANCELLED); - expect(client.scheduler.removeEventFromQueue(event)).toBeFalsy(); + expect(client.scheduler?.removeEventFromQueue(event)).toBeFalsy(); expect(httpLookups.filter(h => h.path.includes("/send/")).length).toBe(0); } it("should cancel an event which is queued", () => { event.setStatus(EventStatus.QUEUED); - client.scheduler.queueEvent(event); + client.scheduler?.queueEvent(event); client.cancelPendingEvent(event); assertCancelled(); }); it("should cancel an event which is encrypting", async () => { + // @ts-ignore protected method access client.encryptAndSendEvent(null, event); await testUtils.emitPromise(event, "Event.status"); client.cancelPendingEvent(event); @@ -1103,7 +1140,7 @@ describe("MatrixClient", function() { const room = { hasPendingEvent: jest.fn().mockReturnValue(false), addLocalEchoReceipt: jest.fn(), - }; + } as unknown as Room; const rrEvent = new MatrixEvent({ event_id: "read_event_id" }); const rpEvent = new MatrixEvent({ event_id: "read_private_event_id" }); client.getRoom = () => room; @@ -1142,7 +1179,7 @@ describe("MatrixClient", function() { const content = makeBeaconInfoContent(100, true); beforeEach(() => { - client.http.authedRequest.mockClear().mockResolvedValue({}); + mocked(client.http.authedRequest).mockClear().mockResolvedValue({}); }); it("creates new beacon info", async () => { @@ -1150,7 +1187,7 @@ describe("MatrixClient", function() { // event type combined const expectedEventType = M_BEACON_INFO.name; - const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0]; + const [method, path, queryParams, requestContent] = mocked(client.http.authedRequest).mock.calls[0]; expect(method).toBe('PUT'); expect(path).toEqual( `/rooms/${encodeURIComponent(roomId)}/state/` + @@ -1164,7 +1201,7 @@ describe("MatrixClient", function() { await client.unstable_setLiveBeacon(roomId, content); // event type combined - const [, path, , requestContent] = client.http.authedRequest.mock.calls[0]; + const [, path, , requestContent] = mocked(client.http.authedRequest).mock.calls[0]; expect(path).toEqual( `/rooms/${encodeURIComponent(roomId)}/state/` + `${encodeURIComponent(M_BEACON_INFO.name)}/${encodeURIComponent(userId)}`, @@ -1242,7 +1279,7 @@ describe("MatrixClient", function() { const newPassword = 'newpassword'; const passwordTest = (expectedRequestContent: any) => { - const [method, path, queryParams, requestContent] = client.http.authedRequest.mock.calls[0]; + const [method, path, queryParams, requestContent] = mocked(client.http.authedRequest).mock.calls[0]; expect(method).toBe('POST'); expect(path).toEqual('/account/password'); expect(queryParams).toBeFalsy(); @@ -1250,7 +1287,7 @@ describe("MatrixClient", function() { }; beforeEach(() => { - client.http.authedRequest.mockClear().mockResolvedValue({}); + mocked(client.http.authedRequest).mockClear().mockResolvedValue({}); }); it("no logout_devices specified", async () => { @@ -1289,13 +1326,13 @@ describe("MatrixClient", function() { const response = { aliases: ["#woop:example.org", "#another:example.org"], }; - client.http.authedRequest.mockClear().mockResolvedValue(response); + mocked(client.http.authedRequest).mockClear().mockResolvedValue(response); const roomId = "!whatever:example.org"; const result = await client.getLocalAliases(roomId); // Current version of the endpoint we support is v3 - const [method, path, queryParams, data, opts] = client.http.authedRequest.mock.calls[0]; + const [method, path, queryParams, data, opts] = mocked(client.http.authedRequest).mock.calls[0]; expect(data).toBeFalsy(); expect(method).toBe('GET'); expect(path).toEqual(`/rooms/${encodeURIComponent(roomId)}/aliases`); @@ -1352,11 +1389,11 @@ describe("MatrixClient", function() { ], username: "1443779631:@user:example.com", password: "JlKfBy1QwLrO20385QyAtEyIv0=", - }; + } as unknown as ITurnServerResponse; jest.spyOn(client, "turnServer").mockResolvedValue(turnServer); const events: any[][] = []; - const onTurnServers = (...args) => events.push(args); + const onTurnServers = (...args: any[]) => events.push(args); client.on(ClientEvent.TurnServers, onTurnServers); expect(await client.checkTurnServers()).toBe(true); client.off(ClientEvent.TurnServers, onTurnServers); @@ -1372,7 +1409,7 @@ describe("MatrixClient", function() { jest.spyOn(client, "turnServer").mockRejectedValue(error); const events: any[][] = []; - const onTurnServersError = (...args) => events.push(args); + const onTurnServersError = (...args: any[]) => events.push(args); client.on(ClientEvent.TurnServersError, onTurnServersError); expect(await client.checkTurnServers()).toBe(false); client.off(ClientEvent.TurnServersError, onTurnServersError); @@ -1384,7 +1421,7 @@ describe("MatrixClient", function() { jest.spyOn(client, "turnServer").mockRejectedValue(error); const events: any[][] = []; - const onTurnServersError = (...args) => events.push(args); + const onTurnServersError = (...args: any[]) => events.push(args); client.on(ClientEvent.TurnServersError, onTurnServersError); expect(await client.checkTurnServers()).toBe(false); client.off(ClientEvent.TurnServersError, onTurnServersError); @@ -1400,7 +1437,7 @@ describe("MatrixClient", function() { it("is an alias for the crypto method", async () => { client.crypto = testUtils.mock(Crypto, "Crypto"); - const deviceInfos = []; + const deviceInfos: IOlmDevice[] = []; const payload = {}; await client.encryptAndSendToDevices(deviceInfos, payload); expect(client.crypto.encryptAndSendToDevices).toHaveBeenLastCalledWith(deviceInfos, payload); @@ -1413,7 +1450,7 @@ describe("MatrixClient", function() { const dataStore = new Map(); client.setAccountData = function(eventType, content) { dataStore.set(eventType, content); - return Promise.resolve(); + return Promise.resolve({}); }; client.getAccountData = function(eventType) { const data = dataStore.get(eventType); @@ -1424,9 +1461,9 @@ describe("MatrixClient", function() { // Mockup `createRoom`/`getRoom`/`joinRoom`, including state. const rooms = new Map(); - client.createRoom = function(options = {}) { + client.createRoom = function(options: Options = {}) { const roomId = options["_roomId"] || `!room-${rooms.size}:example.org`; - const state = new Map(); + const state = new Map(); const room = { roomId, _options: options, @@ -1444,24 +1481,24 @@ describe("MatrixClient", function() { }, }; }, - }; + } as EventTimeline; }, }; }, - }; + } as unknown as WrappedRoom; rooms.set(roomId, room); return Promise.resolve({ room_id: roomId }); }; client.getRoom = function(roomId) { return rooms.get(roomId); }; - client.joinRoom = function(roomId) { - return this.getRoom(roomId) || this.createRoom({ _roomId: roomId }); + client.joinRoom = async function(roomId) { + return this.getRoom(roomId)! || this.createRoom({ _roomId: roomId } as ICreateRoomOpts); }; // Mockup state events client.sendStateEvent = function(roomId, type, content) { - const room = this.getRoom(roomId); + const room = this.getRoom(roomId) as WrappedRoom; const state: Map = room._state; let store = state.get(type); if (!store) { @@ -1480,14 +1517,15 @@ describe("MatrixClient", function() { return content; }, }; - return { event_id: eventId }; + return Promise.resolve({ event_id: eventId }); }; client.redactEvent = function(roomId, eventId) { - const room = this.getRoom(roomId); + const room = this.getRoom(roomId) as WrappedRoom; const state: Map = room._state; for (const store of state.values()) { - delete store[eventId]; + delete store[eventId!]; } + return Promise.resolve({ event_id: "$" + eventId + "-" + Math.random() }); }; }); @@ -1523,7 +1561,7 @@ describe("MatrixClient", function() { roomId: "!snafu:somewhere.org", }); expect(ruleMatch).toBeTruthy(); - expect(ruleMatch.getContent()).toMatchObject({ + expect(ruleMatch!.getContent()).toMatchObject({ recommendation: "m.ban", reason: "just a test", }); @@ -1552,7 +1590,7 @@ describe("MatrixClient", function() { roomId: "!snafu:somewhere.org", }); expect(ruleSenderMatch).toBeTruthy(); - expect(ruleSenderMatch.getContent()).toMatchObject({ + expect(ruleSenderMatch!.getContent()).toMatchObject({ recommendation: "m.ban", reason: REASON, }); @@ -1562,7 +1600,7 @@ describe("MatrixClient", function() { roomId: "!snafu:example.org", }); expect(ruleRoomMatch).toBeTruthy(); - expect(ruleRoomMatch.getContent()).toMatchObject({ + expect(ruleRoomMatch!.getContent()).toMatchObject({ recommendation: "m.ban", reason: REASON, }); @@ -1587,7 +1625,7 @@ describe("MatrixClient", function() { roomId: BAD_ROOM_ID, }); expect(ruleSenderMatch).toBeTruthy(); - expect(ruleSenderMatch.getContent()).toMatchObject({ + expect(ruleSenderMatch!.getContent()).toMatchObject({ recommendation: "m.ban", reason: REASON, }); @@ -1621,7 +1659,7 @@ describe("MatrixClient", function() { roomId: "!snafu:somewhere.org", }); expect(ruleMatch).toBeTruthy(); - expect(ruleMatch.getContent()).toMatchObject({ + expect(ruleMatch!.getContent()).toMatchObject({ recommendation: "m.ban", reason: "just a test", }); @@ -1649,13 +1687,13 @@ describe("MatrixClient", function() { roomId: "!snafu:somewhere.org", }); expect(ruleMatch).toBeTruthy(); - expect(ruleMatch.getContent()).toMatchObject({ + expect(ruleMatch!.getContent()).toMatchObject({ recommendation: "m.ban", reason: "just a test", }); // After removing the invite, we shouldn't reject it anymore. - await client.ignoredInvites.removeRule(ruleMatch); + await client.ignoredInvites.removeRule(ruleMatch as MatrixEvent); const ruleMatch2 = await client.ignoredInvites.getRuleForInvite({ sender: "@foobar:example.org", roomId: "!snafu:somewhere.org", @@ -1669,10 +1707,10 @@ describe("MatrixClient", function() { // Make sure that everything is initialized. await client.ignoredInvites.getOrCreateSourceRooms(); await client.joinRoom(NEW_SOURCE_ROOM_ID); - const newSourceRoom = client.getRoom(NEW_SOURCE_ROOM_ID); + const newSourceRoom = client.getRoom(NEW_SOURCE_ROOM_ID) as WrappedRoom; // Fetch the list of sources and check that we do not have the new room yet. - const policies = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent(); + const policies = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name)!.getContent(); expect(policies).toBeTruthy(); const ignoreInvites = policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name]; expect(ignoreInvites).toBeTruthy(); @@ -1686,7 +1724,7 @@ describe("MatrixClient", function() { expect(added2).toBe(false); // Fetch the list of sources and check that we have added the new room. - const policies2 = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent(); + const policies2 = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name)!.getContent(); expect(policies2).toBeTruthy(); const ignoreInvites2 = policies2[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name]; expect(ignoreInvites2).toBeTruthy(); @@ -1698,7 +1736,7 @@ describe("MatrixClient", function() { // Check where it shows up. const targetRoomId = ignoreInvites2.target; - const targetRoom = client.getRoom(targetRoomId); + const targetRoom = client.getRoom(targetRoomId) as WrappedRoom; expect(targetRoom._state.get(PolicyScope.User)[eventId]).toBeTruthy(); expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy(); }); diff --git a/spec/unit/models/MSC3089TreeSpace.spec.ts b/spec/unit/models/MSC3089TreeSpace.spec.ts index 4158ea8b9..ab39b9075 100644 --- a/spec/unit/models/MSC3089TreeSpace.spec.ts +++ b/spec/unit/models/MSC3089TreeSpace.spec.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient } from "../../../src"; +import { IContent, MatrixClient } from "../../../src"; import { Room } from "../../../src/models/room"; import { MatrixEvent } from "../../../src/models/event"; import { EventType, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../../../src/@types/event"; @@ -33,7 +33,7 @@ describe("MSC3089TreeSpace", () => { const roomId = "!tree:localhost"; const targetUser = "@target:example.org"; - let powerLevels; + let powerLevels: MatrixEvent; beforeEach(() => { // TODO: Use utility functions to create test rooms and clients @@ -480,7 +480,7 @@ describe("MSC3089TreeSpace", () => { const staticDomain = "static.example.org"; function addSubspace(roomId: string, createTs?: number, order?: string) { - const content = { + const content: IContent = { via: [staticDomain], }; if (order) content['order'] = order; diff --git a/spec/unit/models/event.spec.ts b/spec/unit/models/event.spec.ts index 535e0f12d..92d929fd4 100644 --- a/spec/unit/models/event.spec.ts +++ b/spec/unit/models/event.spec.ts @@ -121,7 +121,7 @@ describe('MatrixEvent', () => { }); describe(".attemptDecryption", () => { - let encryptedEvent; + let encryptedEvent: MatrixEvent; const eventId = 'test_encrypted_event'; beforeEach(() => { @@ -155,7 +155,7 @@ describe('MatrixEvent', () => { }, }); }), - }; + } as unknown as Crypto; await encryptedEvent.attemptDecryption(crypto); diff --git a/spec/unit/notifications.spec.ts b/spec/unit/notifications.spec.ts index e7e637d4a..02388f9f0 100644 --- a/spec/unit/notifications.spec.ts +++ b/spec/unit/notifications.spec.ts @@ -39,7 +39,7 @@ let threadEvent: MatrixEvent; const ROOM_ID = "!roomId:example.org"; let THREAD_ID: string; -function mkPushAction(notify, highlight): IActionsObject { +function mkPushAction(notify: boolean, highlight: boolean): IActionsObject { return { notify, tweaks: { diff --git a/spec/unit/read-receipt.spec.ts b/spec/unit/read-receipt.spec.ts index 4443c25be..df902d13b 100644 --- a/spec/unit/read-receipt.spec.ts +++ b/spec/unit/read-receipt.spec.ts @@ -70,7 +70,7 @@ const roomEvent = utils.mkEvent({ }, }); -function mockServerSideSupport(client, serverSideSupport: ServerSupport) { +function mockServerSideSupport(client: MatrixClient, serverSideSupport: ServerSupport) { client.canSupport.set(Feature.ThreadUnreadNotifications, serverSideSupport); } diff --git a/spec/unit/realtime-callbacks.spec.ts b/spec/unit/realtime-callbacks.spec.ts index dd0d605a0..dfbbfc9f7 100644 --- a/spec/unit/realtime-callbacks.spec.ts +++ b/spec/unit/realtime-callbacks.spec.ts @@ -20,7 +20,7 @@ let wallTime = 1234567890; jest.useFakeTimers().setSystemTime(wallTime); describe("realtime-callbacks", function() { - function tick(millis) { + function tick(millis: number): void { wallTime += millis; jest.advanceTimersByTime(millis); } diff --git a/spec/unit/rendezvous/rendezvous.spec.ts b/spec/unit/rendezvous/rendezvous.spec.ts index 2e0f492c0..bca738598 100644 --- a/spec/unit/rendezvous/rendezvous.spec.ts +++ b/spec/unit/rendezvous/rendezvous.spec.ts @@ -87,7 +87,7 @@ describe("Rendezvous", function() { }); let httpBackend: MockHttpBackend; - let fetchFn: typeof global.fetchFn; + let fetchFn: typeof global.fetch; let transports: DummyTransport[]; beforeEach(function() { diff --git a/spec/unit/room-member.spec.ts b/spec/unit/room-member.spec.ts index 8eb3096b6..0ed96339b 100644 --- a/spec/unit/room-member.spec.ts +++ b/spec/unit/room-member.spec.ts @@ -373,37 +373,36 @@ describe("RoomMember", function() { expect(member.events.member).toEqual(joinEvent); }); - it("should set 'name' based on user_id, displayname and room state", - function() { - const roomState = { - getStateEvents: function(type) { - if (type !== "m.room.member") { - return []; - } - return [ - utils.mkMembership({ - event: true, mship: "join", room: roomId, - user: userB, - }), - utils.mkMembership({ - event: true, mship: "join", room: roomId, - user: userC, name: "Alice", - }), - joinEvent, - ]; - }, - getUserIdsWithDisplayName: function(displayName) { - return [userA, userC]; - }, - } as unknown as RoomState; - expect(member.name).toEqual(userA); // default = user_id - member.setMembershipEvent(joinEvent); - expect(member.name).toEqual("Alice"); // prefer displayname - member.setMembershipEvent(joinEvent, roomState); - expect(member.name).not.toEqual("Alice"); // it should disambig. - // user_id should be there somewhere - expect(member.name.indexOf(userA)).not.toEqual(-1); - }); + it("should set 'name' based on user_id, displayname and room state", function() { + const roomState = { + getStateEvents: function(type: string) { + if (type !== "m.room.member") { + return []; + } + return [ + utils.mkMembership({ + event: true, mship: "join", room: roomId, + user: userB, + }), + utils.mkMembership({ + event: true, mship: "join", room: roomId, + user: userC, name: "Alice", + }), + joinEvent, + ]; + }, + getUserIdsWithDisplayName: function(displayName: string) { + return [userA, userC]; + }, + } as unknown as RoomState; + expect(member.name).toEqual(userA); // default = user_id + member.setMembershipEvent(joinEvent); + expect(member.name).toEqual("Alice"); // prefer displayname + member.setMembershipEvent(joinEvent, roomState); + expect(member.name).not.toEqual("Alice"); // it should disambig. + // user_id should be there somewhere + expect(member.name.indexOf(userA)).not.toEqual(-1); + }); it("should emit 'RoomMember.membership' if the membership changes", function() { let emitCount = 0; @@ -455,7 +454,7 @@ describe("RoomMember", function() { }); const roomState = { - getStateEvents: function(type) { + getStateEvents: function(type: string) { if (type !== "m.room.member") { return []; } @@ -467,7 +466,7 @@ describe("RoomMember", function() { joinEvent, ]; }, - getUserIdsWithDisplayName: function(displayName) { + getUserIdsWithDisplayName: function(displayName: string) { return [userA, userC]; }, } as unknown as RoomState; diff --git a/spec/unit/room.spec.ts b/spec/unit/room.spec.ts index f61995703..c820f37a0 100644 --- a/spec/unit/room.spec.ts +++ b/spec/unit/room.spec.ts @@ -19,27 +19,32 @@ limitations under the License. * @module client */ +import { mocked } from "jest-mock"; + import * as utils from "../test-utils/test-utils"; +import { emitPromise } from "../test-utils/test-utils"; import { + Direction, DuplicateStrategy, EventStatus, EventTimelineSet, - EventType, IStateEventWithRoomId, + EventType, IContent, + IStateEventWithRoomId, JoinRule, MatrixEvent, MatrixEventEvent, PendingEventOrdering, RelationType, RoomEvent, + RoomMember, } from "../../src"; import { EventTimeline } from "../../src/models/event-timeline"; import { NotificationCountType, Room } from "../../src/models/room"; import { RoomState } from "../../src/models/room-state"; import { UNSTABLE_ELEMENT_FUNCTIONAL_USERS } from "../../src/@types/event"; import { TestClient } from "../TestClient"; -import { emitPromise } from "../test-utils/test-utils"; import { ReceiptType, WrappedReceipt } from "../../src/@types/read_receipts"; -import { FeatureSupport, Thread, ThreadEvent, THREAD_RELATION_TYPE } from "../../src/models/thread"; +import { FeatureSupport, Thread, THREAD_RELATION_TYPE, ThreadEvent } from "../../src/models/thread"; import { Crypto } from "../../src/crypto"; describe("Room", function() { @@ -48,7 +53,7 @@ describe("Room", function() { const userB = "@bertha:bar"; const userC = "@clarissa:bar"; const userD = "@dorothy:bar"; - let room; + let room: Room; const mkMessage = () => utils.mkMessage({ event: true, @@ -131,13 +136,16 @@ describe("Room", function() { beforeEach(function() { room = new Room(roomId, new TestClient(userA, "device").client, userA); // mock RoomStates + // @ts-ignore room.oldState = room.getLiveTimeline().startState = utils.mock(RoomState, "oldState"); + // @ts-ignore room.currentState = room.getLiveTimeline().endState = utils.mock(RoomState, "currentState"); }); describe('getCreator', () => { it("should return the creator from m.room.create", function() { - room.currentState.getStateEvents.mockImplementation(function(type, key) { + // @ts-ignore - mocked doesn't handle overloads sanely + mocked(room.currentState.getStateEvents).mockImplementation(function(type, key) { if (type === EventType.RoomCreate && key === "") { return utils.mkEvent({ event: true, @@ -160,7 +168,8 @@ describe("Room", function() { const hsUrl = "https://my.home.server"; it("should return the URL from m.room.avatar preferentially", function() { - room.currentState.getStateEvents.mockImplementation(function(type, key) { + // @ts-ignore - mocked doesn't handle overloads sanely + mocked(room.currentState.getStateEvents).mockImplementation(function(type, key) { if (type === EventType.RoomAvatar && key === "") { return utils.mkEvent({ event: true, @@ -174,10 +183,10 @@ describe("Room", function() { }); } }); - const url = room.getAvatarUrl(hsUrl); + const url = room.getAvatarUrl(hsUrl, 100, 100, "scale"); // we don't care about how the mxc->http conversion is done, other // than it contains the mxc body. - expect(url.indexOf("flibble/wibble")).not.toEqual(-1); + expect(url?.indexOf("flibble/wibble")).not.toEqual(-1); }); it("should return nothing if there is no m.room.avatar and allowDefault=false", @@ -189,12 +198,12 @@ describe("Room", function() { describe("getMember", function() { beforeEach(function() { - room.currentState.getMember.mockImplementation(function(userId) { + mocked(room.currentState.getMember).mockImplementation(function(userId) { return { "@alice:bar": { userId: userA, roomId: roomId, - }, + } as unknown as RoomMember, }[userId] || null; }); }); @@ -222,11 +231,13 @@ describe("Room", function() { it("Make sure legacy overload passing options directly as parameters still works", () => { expect(() => room.addLiveEvents(events, DuplicateStrategy.Replace, false)).not.toThrow(); expect(() => room.addLiveEvents(events, DuplicateStrategy.Ignore, true)).not.toThrow(); + // @ts-ignore expect(() => room.addLiveEvents(events, "shouldfailbecauseinvalidduplicatestrategy", false)).toThrow(); }); it("should throw if duplicateStrategy isn't 'replace' or 'ignore'", function() { expect(function() { + // @ts-ignore room.addLiveEvents(events, { duplicateStrategy: "foo", }); @@ -255,6 +266,7 @@ describe("Room", function() { dupe.event.event_id = events[0].getId(); room.addLiveEvents(events); expect(room.timeline[0]).toEqual(events[0]); + // @ts-ignore room.addLiveEvents([dupe], { duplicateStrategy: "ignore", }); @@ -263,7 +275,7 @@ describe("Room", function() { it("should emit 'Room.timeline' events", function() { let callCount = 0; - room.on("Room.timeline", function(event, emitRoom, toStart) { + room.on(RoomEvent.Timeline, function(event, emitRoom, toStart) { callCount += 1; expect(room.timeline.length).toEqual(callCount); expect(event).toEqual(events[callCount - 1]); @@ -306,8 +318,8 @@ describe("Room", function() { userId: userA, membership: "join", name: "Alice", - }; - room.currentState.getSentinelMember.mockImplementation(function(uid) { + } as unknown as RoomMember; + mocked(room.currentState.getSentinelMember).mockImplementation(function(uid) { if (uid === userA) { return sentinel; } @@ -331,27 +343,25 @@ describe("Room", function() { const remoteEventId = remoteEvent.getId(); let callCount = 0; - room.on("Room.localEchoUpdated", - function(event, emitRoom, oldEventId, oldStatus) { - switch (callCount) { - case 0: - expect(event.getId()).toEqual(localEventId); - expect(event.status).toEqual(EventStatus.SENDING); - expect(emitRoom).toEqual(room); - expect(oldEventId).toBeUndefined(); - expect(oldStatus).toBeUndefined(); - break; - case 1: - expect(event.getId()).toEqual(remoteEventId); - expect(event.status).toBeNull(); - expect(emitRoom).toEqual(room); - expect(oldEventId).toEqual(localEventId); - expect(oldStatus).toBe(EventStatus.SENDING); - break; - } - callCount += 1; - }, - ); + room.on(RoomEvent.LocalEchoUpdated, (event, emitRoom, oldEventId, oldStatus) => { + switch (callCount) { + case 0: + expect(event.getId()).toEqual(localEventId); + expect(event.status).toEqual(EventStatus.SENDING); + expect(emitRoom).toEqual(room); + expect(oldEventId).toBeUndefined(); + expect(oldStatus).toBeUndefined(); + break; + case 1: + expect(event.getId()).toEqual(remoteEventId); + expect(event.status).toBeNull(); + expect(emitRoom).toEqual(room); + expect(oldEventId).toEqual(localEventId); + expect(oldStatus).toBe(EventStatus.SENDING); + break; + } + callCount += 1; + }); // first add the local echo room.addPendingEvent(localEvent, "TXN_ID"); @@ -367,7 +377,7 @@ describe("Room", function() { it("should be able to update local echo without a txn ID (/send then /sync)", function() { const eventJson = utils.mkMessage({ room: roomId, user: userA, event: false, - }) as object; + }); delete eventJson["txn_id"]; delete eventJson["event_id"]; const localEvent = new MatrixEvent(Object.assign({ event_id: "$temp" }, eventJson)); @@ -398,7 +408,7 @@ describe("Room", function() { it("should be able to update local echo without a txn ID (/sync then /send)", function() { const eventJson = utils.mkMessage({ room: roomId, user: userA, event: false, - }) as object; + }); delete eventJson["txn_id"]; delete eventJson["event_id"]; const txnId = "My_txn_id"; @@ -483,7 +493,7 @@ describe("Room", function() { it("should emit 'Room.timeline' events when added to the start", function() { let callCount = 0; - room.on("Room.timeline", function(event, emitRoom, toStart) { + room.on(RoomEvent.Timeline, function(event, emitRoom, toStart) { callCount += 1; expect(room.timeline.length).toEqual(callCount); expect(event).toEqual(events[callCount - 1]); @@ -501,19 +511,19 @@ describe("Room", function() { userId: userA, membership: "join", name: "Alice", - }; + } as unknown as RoomMember; const oldSentinel = { userId: userA, membership: "join", name: "Old Alice", - }; - room.currentState.getSentinelMember.mockImplementation(function(uid) { + } as unknown as RoomMember; + mocked(room.currentState.getSentinelMember).mockImplementation(function(uid) { if (uid === userA) { return sentinel; } return null; }); - room.oldState.getSentinelMember.mockImplementation(function(uid) { + mocked(room.oldState.getSentinelMember).mockImplementation(function(uid) { if (uid === userA) { return oldSentinel; } @@ -539,19 +549,19 @@ describe("Room", function() { userId: userA, membership: "join", name: "Alice", - }; + } as unknown as RoomMember; const oldSentinel = { userId: userA, membership: "join", name: "Old Alice", - }; - room.currentState.getSentinelMember.mockImplementation(function(uid) { + } as unknown as RoomMember; + mocked(room.currentState.getSentinelMember).mockImplementation(function(uid) { if (uid === userA) { return sentinel; } return null; }); - room.oldState.getSentinelMember.mockImplementation(function(uid) { + mocked(room.oldState.getSentinelMember).mockImplementation(function(uid) { if (uid === userA) { return oldSentinel; } @@ -599,7 +609,7 @@ describe("Room", function() { }); }); - const resetTimelineTests = function(timelineSupport) { + const resetTimelineTests = function(timelineSupport: boolean) { let events: MatrixEvent[]; beforeEach(function() { @@ -630,8 +640,8 @@ describe("Room", function() { const oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS); const newState = room.getLiveTimeline().getState(EventTimeline.FORWARDS); expect(room.getLiveTimeline().getEvents().length).toEqual(1); - expect(oldState.getStateEvents(EventType.RoomName, "")).toEqual(events[1]); - expect(newState.getStateEvents(EventType.RoomName, "")).toEqual(events[2]); + expect(oldState?.getStateEvents(EventType.RoomName, "")).toEqual(events[1]); + expect(newState?.getStateEvents(EventType.RoomName, "")).toEqual(events[2]); }); it("should reset the legacy timeline fields", function() { @@ -669,17 +679,14 @@ describe("Room", function() { expect(currentStateUpdateEmitCount).toEqual(timelineSupport ? 1 : 0); }); - it("should emit Room.timelineReset event and set the correct " + - "pagination token", function() { + it("should emit Room.timelineReset event and set the correct pagination token", function() { let callCount = 0; - room.on("Room.timelineReset", function(emitRoom) { + room.on(RoomEvent.TimelineReset, function(emitRoom) { callCount += 1; expect(emitRoom).toEqual(room); - // make sure that the pagination token has been set before the - // event is emitted. - const tok = emitRoom.getLiveTimeline() - .getPaginationToken(EventTimeline.BACKWARDS); + // make sure that the pagination token has been set before the event is emitted. + const tok = emitRoom?.getLiveTimeline().getPaginationToken(EventTimeline.BACKWARDS); expect(tok).toEqual("pagToken"); }); @@ -693,7 +700,7 @@ describe("Room", function() { const firstLiveTimeline = room.getLiveTimeline(); room.resetLiveTimeline('sometoken', 'someothertoken'); - const tl = room.getTimelineForEvent(events[0].getId()); + const tl = room.getTimelineForEvent(events[0].getId()!); expect(tl).toBe(timelineSupport ? firstLiveTimeline : null); }); }; @@ -721,30 +728,25 @@ describe("Room", function() { it("should handle events in the same timeline", function() { room.addLiveEvents(events); - expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, - events[1].getId())) + expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!)) .toBeLessThan(0); - expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId()!, - events[1].getId())) + expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[2].getId()!, events[1].getId()!)) .toBeGreaterThan(0); - expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, - events[1].getId())) + expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, events[1].getId()!)) .toEqual(0); }); it("should handle events in adjacent timelines", function() { const oldTimeline = room.addTimeline(); - oldTimeline.setNeighbouringTimeline(room.getLiveTimeline(), 'f'); - room.getLiveTimeline().setNeighbouringTimeline(oldTimeline, 'b'); + oldTimeline.setNeighbouringTimeline(room.getLiveTimeline(), Direction.Forward); + room.getLiveTimeline().setNeighbouringTimeline(oldTimeline, Direction.Backward); room.addEventsToTimeline([events[0]], false, oldTimeline); room.addLiveEvents([events[1]]); - expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, - events[1].getId())) + expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!)) .toBeLessThan(0); - expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, - events[0].getId())) + expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, events[0].getId()!)) .toBeGreaterThan(0); }); @@ -754,11 +756,9 @@ describe("Room", function() { room.addEventsToTimeline([events[0]], false, oldTimeline); room.addLiveEvents([events[1]]); - expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, - events[1].getId())) + expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[0].getId()!, events[1].getId()!)) .toBe(null); - expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, - events[0].getId())) + expect(room.getUnfilteredTimelineSet().compareEventOrdering(events[1].getId()!, events[0].getId()!)) .toBe(null); }); @@ -769,21 +769,21 @@ describe("Room", function() { .compareEventOrdering(events[0].getId()!, "xxx")) .toBe(null); expect(room.getUnfilteredTimelineSet() - .compareEventOrdering("xxx", events[0].getId())) + .compareEventOrdering("xxx", events[0].getId()!)) .toBe(null); expect(room.getUnfilteredTimelineSet() - .compareEventOrdering(events[0].getId()!, events[0].getId())) + .compareEventOrdering(events[0].getId()!, events[0].getId()!)) .toBe(0); }); }); describe("getJoinedMembers", function() { it("should return members whose membership is 'join'", function() { - room.currentState.getMembers.mockImplementation(function() { + mocked(room.currentState.getMembers).mockImplementation(function() { return [ - { userId: "@alice:bar", membership: "join" }, - { userId: "@bob:bar", membership: "invite" }, - { userId: "@cleo:bar", membership: "leave" }, + { userId: "@alice:bar", membership: "join" } as unknown as RoomMember, + { userId: "@bob:bar", membership: "invite" } as unknown as RoomMember, + { userId: "@cleo:bar", membership: "leave" } as unknown as RoomMember, ]; }); const res = room.getJoinedMembers(); @@ -792,9 +792,9 @@ describe("Room", function() { }); it("should return an empty list if no membership is 'join'", function() { - room.currentState.getMembers.mockImplementation(function() { + mocked(room.currentState.getMembers).mockImplementation(function() { return [ - { userId: "@bob:bar", membership: "invite" }, + { userId: "@bob:bar", membership: "invite" } as unknown as RoomMember, ]; }); const res = room.getJoinedMembers(); @@ -805,41 +805,41 @@ describe("Room", function() { describe("hasMembershipState", function() { it("should return true for a matching userId and membership", function() { - room.currentState.getMember.mockImplementation(function(userId) { + mocked(room.currentState.getMember).mockImplementation(function(userId) { return { "@alice:bar": { userId: "@alice:bar", membership: "join" }, "@bob:bar": { userId: "@bob:bar", membership: "invite" }, - }[userId]; + }[userId] as unknown as RoomMember; }); expect(room.hasMembershipState("@bob:bar", "invite")).toBe(true); }); it("should return false if match membership but no match userId", function() { - room.currentState.getMember.mockImplementation(function(userId) { + mocked(room.currentState.getMember).mockImplementation(function(userId) { return { "@alice:bar": { userId: "@alice:bar", membership: "join" }, - }[userId]; + }[userId] as unknown as RoomMember; }); expect(room.hasMembershipState("@bob:bar", "join")).toBe(false); }); it("should return false if match userId but no match membership", function() { - room.currentState.getMember.mockImplementation(function(userId) { + mocked(room.currentState.getMember).mockImplementation(function(userId) { return { "@alice:bar": { userId: "@alice:bar", membership: "join" }, - }[userId]; + }[userId] as unknown as RoomMember; }); expect(room.hasMembershipState("@alice:bar", "ban")).toBe(false); }); it("should return false if no match membership or userId", function() { - room.currentState.getMember.mockImplementation(function(userId) { + mocked(room.currentState.getMember).mockImplementation(function(userId) { return { "@alice:bar": { userId: "@alice:bar", membership: "join" }, - }[userId]; + }[userId] as unknown as RoomMember; }); expect(room.hasMembershipState("@bob:bar", "invite")).toBe(false); }); @@ -1193,8 +1193,8 @@ describe("Room", function() { event: true, }); - function mkReceipt(roomId: string, records) { - const content = {}; + function mkReceipt(roomId: string, records: Array>) { + const content: IContent = {}; records.forEach(function(r) { if (!content[r.eventId]) { content[r.eventId] = {}; @@ -1241,7 +1241,7 @@ describe("Room", function() { it("should emit an event when a receipt is added", function() { const listener = jest.fn(); - room.on("Room.receipt", listener); + room.on(RoomEvent.Receipt, listener); const ts = 13787898424; @@ -1448,7 +1448,7 @@ describe("Room", function() { }); describe("tags", function() { - function mkTags(roomId, tags) { + function mkTags(roomId: string, tags: object) { const content = { "tags": tags }; return new MatrixEvent({ content: content, @@ -1470,7 +1470,7 @@ describe("Room", function() { "received on the event stream", function() { const listener = jest.fn(); - room.on("Room.tags", listener); + room.on(RoomEvent.Tags, listener); const tags = { "m.foo": { "order": 0.5 } }; const event = mkTags(roomId, tags); @@ -1642,11 +1642,14 @@ describe("Room", function() { }); describe("loadMembersIfNeeded", function() { - function createClientMock(serverResponse, storageResponse: MatrixEvent[] | Error | null = null) { + function createClientMock( + serverResponse: Error | MatrixEvent[], + storageResponse: MatrixEvent[] | Error | null = null, + ) { return { getEventMapper: function() { // events should already be MatrixEvents - return function(event) {return event;}; + return function(event: MatrixEvent) {return event;}; }, isCryptoEnabled() { return true; @@ -1671,7 +1674,7 @@ describe("Room", function() { return Promise.resolve(this.storageResponse); } }, - setOutOfBandMembers: function(roomId, memberEvents) { + setOutOfBandMembers: function(roomId: string, memberEvents: IStateEventWithRoomId[]) { this.storedMembers = memberEvents; return Promise.resolve(); }, @@ -2170,7 +2173,7 @@ describe("Room", function() { }, }); - room.createThread("$000", undefined, [eventWithoutARootEvent]); + room.createThread("$000", undefined, [eventWithoutARootEvent], false); const rootEvent = new MatrixEvent({ event_id: "$666", @@ -2188,7 +2191,7 @@ describe("Room", function() { }, }); - expect(() => room.createThread(rootEvent.getId()!, rootEvent, [])).not.toThrow(); + expect(() => room.createThread(rootEvent.getId()!, rootEvent, [], false)).not.toThrow(); }); it("creating thread from edited event should not conflate old versions of the event", () => { @@ -2406,8 +2409,6 @@ describe("Room", function() { Thread.setServerSideListSupport(FeatureSupport.Stable); room.client.createThreadListMessagesRequest = () => Promise.resolve({ - start: null, - end: null, chunk: [], state: [], }); @@ -2761,7 +2762,7 @@ describe("Room", function() { }); describe("thread notifications", () => { - let room; + let room: Room; beforeEach(() => { const client = new TestClient(userA).client; diff --git a/spec/unit/scheduler.spec.ts b/spec/unit/scheduler.spec.ts index 59c2d0a1d..2e7dcf308 100644 --- a/spec/unit/scheduler.spec.ts +++ b/spec/unit/scheduler.spec.ts @@ -1,18 +1,19 @@ // This file had a function whose name is all caps, which displeases eslint /* eslint new-cap: "off" */ -import { defer } from '../../src/utils'; +import { defer, IDeferred } from '../../src/utils'; import { MatrixError } from "../../src/http-api"; import { MatrixScheduler } from "../../src/scheduler"; import * as utils from "../test-utils/test-utils"; +import { MatrixEvent } from "../../src"; jest.useFakeTimers(); describe("MatrixScheduler", function() { - let scheduler; - let retryFn; - let queueFn; - let deferred; + let scheduler: MatrixScheduler>; + let retryFn: Function | null; + let queueFn: ((event: MatrixEvent) => string | null) | null; + let deferred: IDeferred>; const roomId = "!foo:bar"; const eventA = utils.mkMessage({ user: "@alice:bar", room: roomId, event: true, @@ -65,8 +66,8 @@ describe("MatrixScheduler", function() { deferB.resolve({ b: true }); deferA.resolve({ a: true }); const [a, b] = await abPromise; - expect(a.a).toEqual(true); - expect(b.b).toEqual(true); + expect(a!.a).toEqual(true); + expect(b!.b).toEqual(true); }); it("should invoke the retryFn on failure and wait the amount of time specified", @@ -92,6 +93,7 @@ describe("MatrixScheduler", function() { return new Promise(() => {}); } expect(procCount).toBeLessThan(3); + return new Promise(() => {}); }); scheduler.queueEvent(eventA); @@ -119,8 +121,8 @@ describe("MatrixScheduler", function() { return "yep"; }; - const deferA = defer(); - const deferB = defer(); + const deferA = defer>(); + const deferB = defer>(); let procCount = 0; scheduler.setProcessFunction(function(ev) { procCount += 1; @@ -132,6 +134,7 @@ describe("MatrixScheduler", function() { return deferB.promise; } expect(procCount).toBeLessThan(3); + return new Promise>(() => {}); }); const globalA = scheduler.queueEvent(eventA); @@ -159,7 +162,7 @@ describe("MatrixScheduler", function() { const eventC = utils.mkMessage({ user: "@a:bar", room: roomId, event: true }); const eventD = utils.mkMessage({ user: "@b:bar", room: roomId, event: true }); - const buckets = {}; + const buckets: Record = {}; buckets[eventA.getId()!] = "queue_A"; buckets[eventD.getId()!] = "queue_A"; buckets[eventB.getId()!] = "queue_B"; @@ -169,13 +172,13 @@ describe("MatrixScheduler", function() { return 0; }; queueFn = function(event) { - return buckets[event.getId()]; + return buckets[event.getId()!]; }; const expectOrder = [ eventA.getId(), eventB.getId(), eventD.getId(), ]; - const deferA = defer(); + const deferA = defer>(); scheduler.setProcessFunction(function(event) { const id = expectOrder.shift(); expect(id).toEqual(event.getId()); @@ -191,7 +194,7 @@ describe("MatrixScheduler", function() { // wait a bit then resolve A and we should get D (not C) next. setTimeout(function() { - deferA.resolve(); + deferA.resolve({}); }, 1000); jest.advanceTimersByTime(1000); }); @@ -210,7 +213,7 @@ describe("MatrixScheduler", function() { }; const prom = scheduler.queueEvent(eventA); expect(prom).toBeTruthy(); - expect(prom.then).toBeTruthy(); + expect(prom!.then).toBeTruthy(); }); }); @@ -237,15 +240,15 @@ describe("MatrixScheduler", function() { scheduler.queueEvent(eventA); scheduler.queueEvent(eventB); const queue = scheduler.getQueueForEvent(eventA); - expect(queue.length).toEqual(2); + expect(queue).toHaveLength(2); expect(queue).toEqual([eventA, eventB]); // modify the queue const eventC = utils.mkMessage( { user: "@a:bar", room: roomId, event: true }, ); - queue.push(eventC); + queue!.push(eventC); const queueAgain = scheduler.getQueueForEvent(eventA); - expect(queueAgain.length).toEqual(2); + expect(queueAgain).toHaveLength(2); }); it("should return a list of events in the queue and modifications to" + @@ -255,10 +258,10 @@ describe("MatrixScheduler", function() { }; scheduler.queueEvent(eventA); scheduler.queueEvent(eventB); - const queue = scheduler.getQueueForEvent(eventA); - queue[1].event.content.body = "foo"; - const queueAgain = scheduler.getQueueForEvent(eventA); - expect(queueAgain[1].event.content.body).toEqual("foo"); + const queue = scheduler.getQueueForEvent(eventA)!; + queue[1].event.content!.body = "foo"; + const queueAgain = scheduler.getQueueForEvent(eventA)!; + expect(queueAgain[1].event.content?.body).toEqual("foo"); }); }); diff --git a/spec/unit/sync-accumulator.spec.ts b/spec/unit/sync-accumulator.spec.ts index 2e1ec58b6..9a72a6b33 100644 --- a/spec/unit/sync-accumulator.spec.ts +++ b/spec/unit/sync-accumulator.spec.ts @@ -16,7 +16,8 @@ limitations under the License. */ import { ReceiptType } from "../../src/@types/read_receipts"; -import { SyncAccumulator } from "../../src/sync-accumulator"; +import { IJoinedRoom, ISyncResponse, SyncAccumulator } from "../../src/sync-accumulator"; +import { IRoomSummary } from "../../src"; // The event body & unsigned object get frozen to assert that they don't get altered // by the impl @@ -55,10 +56,10 @@ const RES_WITH_AGE = { }, }, }, -}; +} as unknown as ISyncResponse; describe("SyncAccumulator", function() { - let sa; + let sa: SyncAccumulator; beforeEach(function() { sa = new SyncAccumulator({ @@ -98,7 +99,7 @@ describe("SyncAccumulator", function() { }, }, }, - }; + } as unknown as ISyncResponse; sa.accumulate(res); const output = sa.getJSON(); expect(output.nextBatch).toEqual(res.next_batch); @@ -223,7 +224,6 @@ describe("SyncAccumulator", function() { content: { user_ids: ["@alice:localhost"], }, - room_id: "!foo:bar", }], }, }); @@ -281,12 +281,12 @@ describe("SyncAccumulator", function() { account_data: { events: [acc1], }, - }); + } as unknown as ISyncResponse); sa.accumulate({ account_data: { events: [acc2], }, - }); + } as unknown as ISyncResponse); expect( sa.getJSON().accountData.length, ).toEqual(1); @@ -422,7 +422,7 @@ describe("SyncAccumulator", function() { }); describe("summary field", function() { - function createSyncResponseWithSummary(summary) { + function createSyncResponseWithSummary(summary: IRoomSummary): ISyncResponse { return { next_batch: "abc", rooms: { @@ -444,7 +444,7 @@ describe("SyncAccumulator", function() { }, }, }, - }; + } as unknown as ISyncResponse; } afterEach(() => { @@ -487,8 +487,8 @@ describe("SyncAccumulator", function() { jest.spyOn(global.Date, 'now').mockReturnValue(startingTs + delta); const output = sa.getJSON(); - expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned.age).toEqual( - RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0].unsigned.age + delta, + expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned?.age).toEqual( + RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0].unsigned!.age! + delta, ); expect(Object.keys(output.roomsData.join["!foo:bar"].timeline.events[0])).toEqual( Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]), @@ -507,12 +507,12 @@ describe("SyncAccumulator", function() { sa.accumulate(RES_WITH_AGE); const output = sa.getJSON(); expect(output.roomsData.join["!foo:bar"] - .unread_thread_notifications["$143273582443PhrSn:example.org"]).not.toBeUndefined(); + .unread_thread_notifications!["$143273582443PhrSn:example.org"]).not.toBeUndefined(); }); }); }); -function syncSkeleton(joinObj) { +function syncSkeleton(joinObj: Partial): ISyncResponse { joinObj = joinObj || {}; return { next_batch: "abc", @@ -521,11 +521,12 @@ function syncSkeleton(joinObj) { "!foo:bar": joinObj, }, }, - }; + } as unknown as ISyncResponse; } -function msg(localpart, text) { +function msg(localpart: string, text: string) { return { + event_id: "$" + Math.random(), content: { body: text, }, @@ -535,8 +536,9 @@ function msg(localpart, text) { }; } -function member(localpart, membership) { +function member(localpart: string, membership: string) { return { + event_id: "$" + Math.random(), content: { membership: membership, }, diff --git a/spec/unit/utils.spec.ts b/spec/unit/utils.spec.ts index 1b11f2a7c..c9fc75c3e 100644 --- a/spec/unit/utils.spec.ts +++ b/spec/unit/utils.spec.ts @@ -455,7 +455,11 @@ describe("utils", function() { describe("recursivelyAssign", () => { it("doesn't override with null/undefined", () => { - const result = utils.recursivelyAssign( + const result = utils.recursivelyAssign<{ + string: string; + object: object; + float: number; + }, {}>( { string: "Hello world", object: {}, @@ -475,7 +479,11 @@ describe("utils", function() { }); it("assigns recursively", () => { - const result = utils.recursivelyAssign( + const result = utils.recursivelyAssign<{ + number: number; + object: object; + thing: string | object; + }, {}>( { number: 42, object: { diff --git a/spec/unit/webrtc/call.spec.ts b/spec/unit/webrtc/call.spec.ts index d1cb5d296..1d4f9f034 100644 --- a/spec/unit/webrtc/call.spec.ts +++ b/spec/unit/webrtc/call.spec.ts @@ -933,7 +933,7 @@ describe('Call', function() { await fakeIncomingCall(client, call, "1"); }); - const untilEventSent = async (...args) => { + const untilEventSent = async (...args: any[]) => { const maxTries = 20; for (let tries = 0; tries < maxTries; ++tries) { @@ -971,7 +971,7 @@ describe('Call', function() { }); describe("ICE candidate sending", () => { - let mockPeerConn; + let mockPeerConn: MockRTCPeerConnection; const fakeCandidateString = "here is a fake candidate!"; const fakeCandidateEvent = { candidate: { @@ -1086,7 +1086,7 @@ describe('Call', function() { }); describe("Screen sharing", () => { - const waitNegotiateFunc = resolve => { + const waitNegotiateFunc = (resolve: Function): void => { mockSendEvent.mockImplementationOnce(() => { // Note that the peer connection here is a dummy one and always returns // dummy SDP, so there's not much point returning the content: the SDP will diff --git a/src/@types/read_receipts.ts b/src/@types/read_receipts.ts index 689313672..ea8329260 100644 --- a/src/@types/read_receipts.ts +++ b/src/@types/read_receipts.ts @@ -42,7 +42,7 @@ export type ReceiptCache = {[eventId: string]: CachedReceipt[]}; export interface ReceiptContent { [eventId: string]: { - [key in ReceiptType]: { + [key in ReceiptType | string]: { [userId: string]: Receipt; }; }; diff --git a/src/autodiscovery.ts b/src/autodiscovery.ts index d26e4d5c6..fe2b6e383 100644 --- a/src/autodiscovery.ts +++ b/src/autodiscovery.ts @@ -49,7 +49,7 @@ interface WellKnownConfig extends Omit { error?: IWellKnownConfig["error"] | null; } -interface ClientConfig { +interface ClientConfig extends Omit { "m.homeserver": WellKnownConfig; "m.identity_server": WellKnownConfig; } @@ -131,7 +131,7 @@ export class AutoDiscovery { * configuration, which may include error states. Rejects on unexpected * failure, not when verification fails. */ - public static async fromDiscoveryConfig(wellknown: any): Promise { + public static async fromDiscoveryConfig(wellknown: IClientWellKnown): Promise { // Step 1 is to get the config, which is provided to us here. // We default to an error state to make the first few checks easier to @@ -185,7 +185,7 @@ export class AutoDiscovery { const hsVersions = await this.fetchWellKnownObject( `${hsUrl}/_matrix/client/versions`, ); - if (!hsVersions || !hsVersions.raw["versions"]) { + if (!hsVersions || !hsVersions.raw?.["versions"]) { logger.error("Invalid /versions response"); clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER; @@ -219,9 +219,7 @@ export class AutoDiscovery { // Step 5a: Make sure the URL is valid *looking*. We'll make sure it // points to an identity server in Step 5b. - isUrl = this.sanitizeWellKnownUrl( - wellknown["m.identity_server"]["base_url"], - ); + isUrl = this.sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]); if (!isUrl) { logger.error("Invalid base_url for m.identity_server"); failingClientConfig["m.identity_server"].error = @@ -234,7 +232,7 @@ export class AutoDiscovery { const isResponse = await this.fetchWellKnownObject( `${isUrl}/_matrix/identity/api/v1`, ); - if (!isResponse || !isResponse.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) { + if (!isResponse?.raw || isResponse.action !== AutoDiscoveryAction.SUCCESS) { logger.error("Invalid /api/v1 response"); failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER; @@ -259,14 +257,16 @@ export class AutoDiscovery { // Step 7: Copy any other keys directly into the clientConfig. This is for // things like custom configuration of services. - Object.keys(wellknown).forEach((k) => { + Object.keys(wellknown).forEach((k: keyof IClientWellKnown) => { if (k === "m.homeserver" || k === "m.identity_server") { // Only copy selected parts of the config to avoid overwriting // properties computed by the validation logic above. const notProps = ["error", "state", "base_url"]; - for (const prop of Object.keys(wellknown[k])) { + for (const prop of Object.keys(wellknown[k]!)) { if (notProps.includes(prop)) continue; - clientConfig[k][prop] = wellknown[k][prop]; + type Prop = Exclude; + // @ts-ignore - ts gets unhappy as we're mixing types here + clientConfig[k][prop as Prop] = wellknown[k]![prop as Prop]; } } else { // Just copy the whole thing over otherwise @@ -347,7 +347,7 @@ export class AutoDiscovery { } // Step 2: Validate and parse the config - return AutoDiscovery.fromDiscoveryConfig(wellknown.raw); + return AutoDiscovery.fromDiscoveryConfig(wellknown.raw!); } /** @@ -378,7 +378,7 @@ export class AutoDiscovery { * @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid. * @private */ - private static sanitizeWellKnownUrl(url: string): string | false { + private static sanitizeWellKnownUrl(url?: string | null): string | false { if (!url) return false; try { diff --git a/src/browser-index.js b/src/browser-index.ts similarity index 76% rename from src/browser-index.js rename to src/browser-index.ts index 86e887bd4..200b2a32d 100644 --- a/src/browser-index.js +++ b/src/browser-index.ts @@ -16,27 +16,28 @@ limitations under the License. import * as matrixcs from "./matrix"; +type BrowserMatrix = typeof matrixcs; +declare global { + /* eslint-disable no-var, camelcase */ + var __js_sdk_entrypoint: boolean; + var matrixcs: BrowserMatrix; + /* eslint-enable no-var */ +} + if (global.__js_sdk_entrypoint) { throw new Error("Multiple matrix-js-sdk entrypoints detected!"); } global.__js_sdk_entrypoint = true; -// just *accessing* indexedDB throws an exception in firefox with -// indexeddb disabled. -let indexedDB; +// just *accessing* indexedDB throws an exception in firefox with indexeddb disabled. +let indexedDB: IDBFactory | undefined; try { indexedDB = global.indexedDB; } catch (e) {} // if our browser (appears to) support indexeddb, use an indexeddb crypto store. if (indexedDB) { - matrixcs.setCryptoStoreFactory( - function() { - return new matrixcs.IndexedDBCryptoStore( - indexedDB, "matrix-js-sdk:crypto", - ); - }, - ); + matrixcs.setCryptoStoreFactory(() => new matrixcs.IndexedDBCryptoStore(indexedDB!, "matrix-js-sdk:crypto")); } // We export 3 things to make browserify happy as well as downstream projects. diff --git a/src/client.ts b/src/client.ts index 9abcd967f..fae73b7a4 100644 --- a/src/client.ts +++ b/src/client.ts @@ -530,7 +530,7 @@ export interface IPreviewUrlResponse { "matrix:image:size"?: number; } -interface ITurnServerResponse { +export interface ITurnServerResponse { uris: string[]; username: string; password: string; @@ -561,7 +561,7 @@ export interface IClientWellKnown { } export interface IWellKnownConfig { - raw?: any; // todo typings + raw?: IClientWellKnown; action?: AutoDiscoveryAction; reason?: string; error?: Error | string; @@ -605,10 +605,10 @@ interface ITagMetadata { } interface IMessagesResponse { - start: string; - end: string; + start?: string; + end?: string; chunk: IRoomEvent[]; - state: IStateEvent[]; + state?: IStateEvent[]; } interface IThreadedMessagesResponse { @@ -635,6 +635,17 @@ export interface IUploadKeysRequest { "org.matrix.msc2732.fallback_keys"?: Record; } +export interface IQueryKeysRequest { + device_keys: { [userId: string]: string[] }; + timeout?: number; + token?: string; +} + +export interface IClaimKeysRequest { + one_time_keys: { [userId: string]: { [deviceId: string]: string } }; + timeout?: number; +} + export interface IOpenIDToken { access_token: string; token_type: "Bearer" | string; @@ -1744,13 +1755,15 @@ export class MatrixClient extends TypedEventEmitter(Method.Get, "/capabilities").catch((e: Error): void => { + }; + return this.http.authedRequest(Method.Get, "/capabilities").catch((e: Error): Response => { // We swallow errors because we need a default object anyhow logger.error(e); + return {}; }).then((r = {}) => { - const capabilities: ICapabilities = r["capabilities"] || {}; + const capabilities = r["capabilities"] || {}; // If the capabilities missed the cache, cache it for a shorter amount // of time to try and refresh them later. @@ -3229,28 +3242,28 @@ export class MatrixClient extends TypedEventEmitter; public restoreKeyBackupWithRecoveryKey( recoveryKey: string, targetRoomId: string, targetSessionId: undefined, backupInfo: IKeyBackupInfo, - opts: IKeyBackupRestoreOpts, + opts?: IKeyBackupRestoreOpts, ): Promise; public restoreKeyBackupWithRecoveryKey( recoveryKey: string, targetRoomId: string, targetSessionId: string, backupInfo: IKeyBackupInfo, - opts: IKeyBackupRestoreOpts, + opts?: IKeyBackupRestoreOpts, ): Promise; public restoreKeyBackupWithRecoveryKey( recoveryKey: string, targetRoomId: string | undefined, targetSessionId: string | undefined, backupInfo: IKeyBackupInfo, - opts: IKeyBackupRestoreOpts, + opts?: IKeyBackupRestoreOpts, ): Promise { const privKey = decodeRecoveryKey(recoveryKey); return this.restoreKeyBackup(privKey, targetRoomId!, targetSessionId!, backupInfo, opts); @@ -3442,7 +3455,7 @@ export class MatrixClient extends TypedEventEmitter = {}; for (const [userId, devices] of Object.entries(deviceInfos)) { devicesByUser[userId] = Object.values(devices); } @@ -3616,7 +3629,7 @@ export class MatrixClient extends TypedEventEmitter { - const content = { ignored_users: {} }; + const content = { ignored_users: {} as Record }; userIds.forEach((u) => { content.ignored_users[u] = {}; }); @@ -3910,16 +3923,25 @@ export class MatrixClient extends TypedEventEmitter; public sendEvent( roomId: string, - threadId: string | null, - eventType: string | IContent, - content?: IContent | string, - txnId?: string, + threadIdOrEventType: string | null, + eventTypeOrContent: string | IContent, + contentOrTxnId?: IContent | string, + txnIdOrVoid?: string, ): Promise { - if (!threadId?.startsWith(EVENT_ID_PREFIX) && threadId !== null) { - txnId = content as string; - content = eventType as IContent; - eventType = threadId; + let threadId: string | null; + let eventType: string; + let content: IContent; + let txnId: string | undefined; + if (!threadIdOrEventType?.startsWith(EVENT_ID_PREFIX) && threadIdOrEventType !== null) { + txnId = contentOrTxnId as string; + content = eventTypeOrContent as IContent; + eventType = threadIdOrEventType; threadId = null; + } else { + txnId = txnIdOrVoid; + content = contentOrTxnId as IContent; + eventType = eventTypeOrContent as string; + threadId = threadIdOrEventType; } // If we expect that an event is part of a thread but is missing the relation @@ -4280,7 +4302,7 @@ export class MatrixClient extends TypedEventEmitter | undefined => { - let newEvent: IPartialEvent | undefined; + let newEvent: IPartialEvent | undefined; if (content['msgtype'] === MsgType.Text) { newEvent = MessageEvent.from(content['body'], content['formatted_body']).serialize(); @@ -5262,11 +5284,11 @@ export class MatrixClient extends TypedEventEmitter { @@ -6261,7 +6283,7 @@ export class MatrixClient extends TypedEventEmitter | undefined { + public setRoomMutePushRule(scope: "global" | "device", roomId: string, mute: boolean): Promise | undefined { let promise: Promise | undefined; let hasDontNotifyRule = false; @@ -6818,7 +6840,7 @@ export class MatrixClient extends TypedEventEmitter { return primTypes.includes(typeof value); }) - .reduce((obj, [key, value]) => { + .reduce>((obj, [key, value]) => { obj[key] = value; return obj; }, {}); @@ -7841,7 +7863,7 @@ export class MatrixClient extends TypedEventEmitter { - const content: any = { + const content: IQueryKeysRequest = { device_keys: {}, }; if ('token' in opts) { @@ -8582,7 +8604,7 @@ export class MatrixClient extends TypedEventEmitter(Method.Post, "/lookup", params, IdentityPrefix.V2, identityAccessToken); - if (!response || !response['mappings']) return []; // no results + if (!response?.['mappings']) return []; // no results const foundAddresses: { address: string, mxid: string }[] = []; for (const hashed of Object.keys(response['mappings'])) { @@ -9028,7 +9049,7 @@ export class MatrixClient extends TypedEventEmitter { + const targets = Object.keys(contentMap).reduce>((obj, key) => { obj[key] = Object.keys(contentMap[key]); return obj; }, {}); diff --git a/src/crypto/CrossSigning.ts b/src/crypto/CrossSigning.ts index e776b93ad..4035db04c 100644 --- a/src/crypto/CrossSigning.ts +++ b/src/crypto/CrossSigning.ts @@ -84,6 +84,7 @@ export class CrossSigningInfo { const res = new CrossSigningInfo(userId); for (const prop in obj) { if (obj.hasOwnProperty(prop)) { + // @ts-ignore - ts doesn't like this and nor should we res[prop] = obj[prop]; } } diff --git a/src/crypto/EncryptionSetup.ts b/src/crypto/EncryptionSetup.ts index 253c83c1d..a46afdc4d 100644 --- a/src/crypto/EncryptionSetup.ts +++ b/src/crypto/EncryptionSetup.ts @@ -35,7 +35,7 @@ import { IAccountDataClient } from "./SecretStorage"; interface ICrossSigningKeys { authUpload: IBootstrapCrossSigningOpts["authUploadDeviceSigningKeys"]; - keys: Record; + keys: Record<"master" | "self_signing" | "user_signing", ICrossSigningKey>; } /** @@ -209,7 +209,7 @@ export class EncryptionSetupOperation { if (this.crossSigningKeys) { const keys: Partial = {}; for (const [name, key] of Object.entries(this.crossSigningKeys.keys)) { - keys[name + "_key"] = key; + keys[((name as keyof ICrossSigningKeys["keys"]) + "_key" as keyof CrossSigningKeys)] = key; } // We must only call `uploadDeviceSigningKeys` from inside this auth diff --git a/src/crypto/OlmDevice.ts b/src/crypto/OlmDevice.ts index efa34ad41..13463ef11 100644 --- a/src/crypto/OlmDevice.ts +++ b/src/crypto/OlmDevice.ts @@ -23,11 +23,19 @@ import { CryptoStore, IProblem, ISessionInfo, IWithheld } from "./store/base"; import { IOlmDevice, IOutboundGroupSessionKey } from "./algorithms/megolm"; import { IMegolmSessionData } from "./index"; import { OlmGroupSessionExtraData } from "../@types/crypto"; +import { IMessage } from "./algorithms/olm"; // The maximum size of an event is 65K, and we base64 the content, so this is a // reasonable approximation to the biggest plaintext we can encrypt. const MAX_PLAINTEXT_LENGTH = 65536 * 3 / 4; +export class PayloadTooLargeError extends Error { + public readonly data = { + errcode: "M_TOO_LARGE", + error: "Payload too large for encrypted message", + }; +} + function checkPayloadLength(payloadString: string): void { if (payloadString === undefined) { throw new Error("payloadString undefined"); @@ -40,15 +48,8 @@ function checkPayloadLength(payloadString: string): void { // Note that even if we manage to do the encryption, the message send may fail, // because by the time we've wrapped the ciphertext in the event object, it may // exceed 65K. But at least we won't just fail with "abort()" in that case. - const err = new Error("Message too long (" + payloadString.length + " bytes). " + - "The maximum for an encrypted message is " + - MAX_PLAINTEXT_LENGTH + " bytes."); - // TODO: [TypeScript] We should have our own error types - err["data"] = { - errcode: "M_TOO_LARGE", - error: "Payload too large for encrypted message", - }; - throw err; + throw new PayloadTooLargeError(`Message too long (${payloadString.length} bytes). ` + + `The maximum for an encrypted message is ${MAX_PLAINTEXT_LENGTH} bytes.`); } } @@ -126,6 +127,8 @@ interface IInboundGroupSessionKey { } /* eslint-enable camelcase */ +type OneTimeKeys = { curve25519: { [keyId: string]: string } }; + /** * Manages the olm cryptography functions. Each OlmDevice has a single * OlmAccount and a number of OlmSessions. @@ -452,7 +455,7 @@ export class OlmDevice { * @return {Promise} base64-encoded signature */ public async sign(message: string): Promise { - let result; + let result: string; await this.cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { @@ -460,7 +463,7 @@ export class OlmDevice { result = account.sign(message); }); }); - return result; + return result!; } /** @@ -470,8 +473,8 @@ export class OlmDevice { * curve25519, which is itself an object mapping key id to Curve25519 * key. */ - public async getOneTimeKeys(): Promise<{ curve25519: { [keyId: string]: string } }> { - let result; + public async getOneTimeKeys(): Promise { + let result: OneTimeKeys; await this.cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => { @@ -481,7 +484,7 @@ export class OlmDevice { }, ); - return result; + return result!; } /** @@ -821,10 +824,10 @@ export class OlmDevice { theirDeviceIdentityKey: string, sessionId: string, payloadString: string, - ): Promise { + ): Promise { checkPayloadLength(payloadString); - let res; + let res: IMessage; await this.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { @@ -840,7 +843,7 @@ export class OlmDevice { }, logger.withPrefix("[encryptMessage]"), ); - return res; + return res!; } /** @@ -860,7 +863,7 @@ export class OlmDevice { messageType: number, ciphertext: string, ): Promise { - let payloadString; + let payloadString: string; await this.cryptoStore.doTxn( 'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { @@ -877,7 +880,7 @@ export class OlmDevice { }, logger.withPrefix("[decryptMessage]"), ); - return payloadString; + return payloadString!; } /** @@ -902,7 +905,7 @@ export class OlmDevice { return false; } - let matches; + let matches: boolean; await this.cryptoStore.doTxn( 'readonly', [IndexedDBCryptoStore.STORE_SESSIONS], (txn) => { @@ -912,7 +915,7 @@ export class OlmDevice { }, logger.withPrefix("[matchesSession]"), ); - return matches; + return matches!; } public async recordSessionProblem(deviceKey: string, type: string, fixed: boolean): Promise { @@ -1293,7 +1296,7 @@ export class OlmDevice { result = null; return; } - let res; + let res: ReturnType; try { res = session.decrypt(body); } catch (e) { @@ -1313,8 +1316,8 @@ export class OlmDevice { let plaintext: string = res.plaintext; if (plaintext === undefined) { - // Compatibility for older olm versions. - plaintext = res; + // @ts-ignore - Compatibility for older olm versions. + plaintext = res as string; } else { // Check if we have seen this message index before to detect replay attacks. // If the event ID and timestamp are specified, and the match the event ID @@ -1555,7 +1558,7 @@ export class OlmDevice { } } -export const WITHHELD_MESSAGES = { +export const WITHHELD_MESSAGES: Record = { "m.unverified": "The sender has disabled encrypting to unverified devices.", "m.blacklisted": "The sender has blocked you.", "m.unauthorised": "You are not authorised to read the message.", diff --git a/src/crypto/SecretStorage.ts b/src/crypto/SecretStorage.ts index a7e1e7d73..54702034f 100644 --- a/src/crypto/SecretStorage.ts +++ b/src/crypto/SecretStorage.ts @@ -18,10 +18,9 @@ import { v4 as uuidv4 } from 'uuid'; import { logger } from '../logger'; import * as olmlib from './olmlib'; -import { encodeBase64 } from './olmlib'; import { randomString } from '../randomstring'; import { calculateKeyCheck, decryptAES, encryptAES, IEncryptedPayload } from './aes'; -import { ICryptoCallbacks } from "."; +import { ICryptoCallbacks, IEncryptedContent } from "."; import { IContent, MatrixEvent } from "../models/event"; import { ClientEvent, ClientEventHandlerMap, MatrixClient } from "../client"; import { IAddSecretStorageKeyOpts, ISecretStorageKeyInfo } from './api'; @@ -61,8 +60,7 @@ interface IDecryptors { interface ISecretInfo { encrypted: { - // eslint-disable-next-line camelcase - key_id: IEncryptedPayload; + [keyId: string]: IEncryptedPayload; }; } @@ -318,23 +316,11 @@ export class SecretStorage { `the keys it is encrypted with are for a supported algorithm`); } - let keyId: string; - let decryption; - try { - // fetch private key from app - [keyId, decryption] = await this.getSecretStorageKey(keys, name); + // fetch private key from app + const [keyId, decryption] = await this.getSecretStorageKey(keys, name); + const encInfo = secretInfo.encrypted[keyId]; - const encInfo = secretInfo.encrypted[keyId]; - - // We don't actually need the decryption object if it's a passthrough - // since we just want to return the key itself. It must be base64 - // encoded, since this is how a key would normally be stored. - if (encInfo.passthrough) return encodeBase64(decryption.get_private_key()); - - return decryption.decrypt(encInfo); - } finally { - if (decryption && decryption.free) decryption.free(); - } + return decryption.decrypt(encInfo); } /** @@ -351,7 +337,7 @@ export class SecretStorage { const secretInfo = await this.accountDataAdapter.getAccountDataFromServer(name); if (!secretInfo?.encrypted) return null; - const ret = {}; + const ret: Record = {}; // filter secret encryption keys with supported algorithm for (const keyId of Object.keys(secretInfo.encrypted)) { @@ -391,7 +377,7 @@ export class SecretStorage { requesting_device_id: this.baseApis.deviceId, request_id: requestId, }; - const toDevice = {}; + const toDevice: Record = {}; for (const device of devices) { toDevice[device] = cancelData; } @@ -412,7 +398,7 @@ export class SecretStorage { request_id: requestId, [ToDeviceMessageId]: uuidv4(), }; - const toDevice = {}; + const toDevice: Record = {}; for (const device of devices) { toDevice[device] = requestData; } @@ -490,9 +476,9 @@ export class SecretStorage { secret: secret, }, }; - const encryptedContent = { + const encryptedContent: IEncryptedContent = { algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.baseApis.crypto!.olmDevice.deviceCurve25519Key, + sender_key: this.baseApis.crypto!.olmDevice.deviceCurve25519Key!, ciphertext: {}, [ToDeviceMessageId]: uuidv4(), }; diff --git a/src/crypto/algorithms/base.ts b/src/crypto/algorithms/base.ts index d6c70bc10..32b9ab73b 100644 --- a/src/crypto/algorithms/base.ts +++ b/src/crypto/algorithms/base.ts @@ -23,8 +23,14 @@ limitations under the License. import { MatrixClient } from "../../client"; import { Room } from "../../models/room"; import { OlmDevice } from "../OlmDevice"; -import { MatrixEvent, RoomMember } from "../../matrix"; -import { Crypto, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from ".."; +import { IContent, MatrixEvent, RoomMember } from "../../matrix"; +import { + Crypto, + IEncryptedContent, + IEventDecryptionResult, + IMegolmSessionData, + IncomingRoomKeyRequest, +} from ".."; import { DeviceInfo } from "../deviceinfo"; import { IRoomEncryption } from "../RoomList"; @@ -108,7 +114,7 @@ export abstract class EncryptionAlgorithm { * * @return {Promise} Promise which resolves to the new event body */ - public abstract encryptMessage(room: Room, eventType: string, content: object): Promise; + public abstract encryptMessage(room: Room, eventType: string, content: IContent): Promise; /** * Called when the membership of a member of the room changes. diff --git a/src/crypto/algorithms/megolm.ts b/src/crypto/algorithms/megolm.ts index 9ba33e32a..b099ef40a 100644 --- a/src/crypto/algorithms/megolm.ts +++ b/src/crypto/algorithms/megolm.ts @@ -38,9 +38,15 @@ import { Room } from '../../models/room'; import { DeviceInfo } from "../deviceinfo"; import { IOlmSessionResult } from "../olmlib"; import { DeviceInfoMap } from "../DeviceList"; -import { MatrixEvent } from "../../models/event"; +import { IContent, MatrixEvent } from "../../models/event"; import { EventType, MsgType, ToDeviceMessageId } from '../../@types/event'; -import { IEncryptedContent, IEventDecryptionResult, IMegolmSessionData, IncomingRoomKeyRequest } from "../index"; +import { + IMegolmEncryptedContent, + IEventDecryptionResult, + IMegolmSessionData, + IncomingRoomKeyRequest, + IEncryptedContent, +} from "../index"; import { RoomKeyRequestState } from '../OutgoingRoomKeyRequestManager'; import { OlmGroupSessionExtraData } from "../../@types/crypto"; import { MatrixError } from "../../http-api"; @@ -228,7 +234,7 @@ class OutboundSessionInfo { * @param {object} params parameters, as per * {@link module:crypto/algorithms/EncryptionAlgorithm} */ -class MegolmEncryption extends EncryptionAlgorithm { +export class MegolmEncryption extends EncryptionAlgorithm { // the most recent attempt to set up a session. This is used to serialise // the session setups, so that we have a race-free view of which session we // are using, and which devices we have shared the keys with. It resolves @@ -761,9 +767,9 @@ class MegolmEncryption extends EncryptionAlgorithm { }, }; - const encryptedContent = { + const encryptedContent: IEncryptedContent = { algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key, + sender_key: this.olmDevice.deviceCurve25519Key!, ciphertext: {}, [ToDeviceMessageId]: uuidv4(), }; @@ -1010,7 +1016,7 @@ class MegolmEncryption extends EncryptionAlgorithm { * * @return {Promise} Promise which resolves to the new event body */ - public async encryptMessage(room: Room, eventType: string, content: object): Promise { + public async encryptMessage(room: Room, eventType: string, content: IContent): Promise { logger.log(`Starting to encrypt event for ${this.roomId}`); if (this.encryptionPreparation != null) { @@ -1045,12 +1051,10 @@ class MegolmEncryption extends EncryptionAlgorithm { content: content, }; - const ciphertext = this.olmDevice.encryptGroupMessage( - session.sessionId, JSON.stringify(payloadJson), - ); - const encryptedContent = { + const ciphertext = this.olmDevice.encryptGroupMessage(session.sessionId, JSON.stringify(payloadJson)); + const encryptedContent: IEncryptedContent = { algorithm: olmlib.MEGOLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key, + sender_key: this.olmDevice.deviceCurve25519Key!, ciphertext: ciphertext, session_id: session.sessionId, // Include our device ID so that recipients can send us a @@ -1064,7 +1068,7 @@ class MegolmEncryption extends EncryptionAlgorithm { return encryptedContent; } - private isVerificationEvent(eventType: string, content: object): boolean { + private isVerificationEvent(eventType: string, content: IContent): boolean { switch (eventType) { case EventType.KeyVerificationCancel: case EventType.KeyVerificationDone: @@ -1227,7 +1231,7 @@ class MegolmEncryption extends EncryptionAlgorithm { * @param {object} params parameters, as per * {@link module:crypto/algorithms/DecryptionAlgorithm} */ -class MegolmDecryption extends DecryptionAlgorithm { +export class MegolmDecryption extends DecryptionAlgorithm { // events which we couldn't decrypt due to unknown sessions / // indexes, or which we could only decrypt with untrusted keys: // map from senderKey|sessionId to Set of MatrixEvents @@ -1670,9 +1674,9 @@ class MegolmDecryption extends DecryptionAlgorithm { await olmlib.ensureOlmSessionsForDevices( this.olmDevice, this.baseApis, { [sender]: [device] }, false, ); - const encryptedContent = { + const encryptedContent: IEncryptedContent = { algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key, + sender_key: this.olmDevice.deviceCurve25519Key!, ciphertext: {}, [ToDeviceMessageId]: uuidv4(), }; @@ -1752,9 +1756,9 @@ class MegolmDecryption extends DecryptionAlgorithm { body.room_id, body.sender_key, body.session_id, ); }).then((payload) => { - const encryptedContent = { + const encryptedContent: IEncryptedContent = { algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key, + sender_key: this.olmDevice.deviceCurve25519Key!, ciphertext: {}, [ToDeviceMessageId]: uuidv4(), }; diff --git a/src/crypto/algorithms/olm.ts b/src/crypto/algorithms/olm.ts index 682aa4c7c..114368319 100644 --- a/src/crypto/algorithms/olm.ts +++ b/src/crypto/algorithms/olm.ts @@ -30,13 +30,13 @@ import { registerAlgorithm, } from "./base"; import { Room } from '../../models/room'; -import { MatrixEvent } from "../../models/event"; -import { IEventDecryptionResult } from "../index"; +import { IContent, MatrixEvent } from "../../models/event"; +import { IEncryptedContent, IEventDecryptionResult, IOlmEncryptedContent } from "../index"; import { IInboundSession } from "../OlmDevice"; const DeviceVerification = DeviceInfo.DeviceVerification; -interface IMessage { +export interface IMessage { type: number; body: string; } @@ -91,7 +91,7 @@ class OlmEncryption extends EncryptionAlgorithm { * * @return {Promise} Promise which resolves to the new event body */ - public async encryptMessage(room: Room, eventType: string, content: object): Promise { + public async encryptMessage(room: Room, eventType: string, content: IContent): Promise { // pick the list of recipients based on the membership list. // // TODO: there is a race condition here! What if a new user turns up @@ -111,9 +111,9 @@ class OlmEncryption extends EncryptionAlgorithm { content: content, }; - const encryptedContent = { + const encryptedContent: IEncryptedContent = { algorithm: olmlib.OLM_ALGORITHM, - sender_key: this.olmDevice.deviceCurve25519Key, + sender_key: this.olmDevice.deviceCurve25519Key!, ciphertext: {}, }; diff --git a/src/crypto/backup.ts b/src/crypto/backup.ts index f2160165b..519128936 100644 --- a/src/crypto/backup.ts +++ b/src/crypto/backup.ts @@ -93,7 +93,7 @@ interface BackupAlgorithmClass { interface BackupAlgorithm { untrusted: boolean; - encryptSession(data: Record): Promise; + encryptSession(data: Record): Promise; decryptSessions(ciphertexts: Record): Promise; authData: AuthData; keyMatches(key: ArrayLike): Promise; @@ -663,7 +663,7 @@ export class Curve25519 implements BackupAlgorithm { public get untrusted(): boolean { return true; } - public async encryptSession(data: Record): Promise { + public async encryptSession(data: Record): Promise { const plainText: Record = Object.assign({}, data); delete plainText.session_id; delete plainText.room_id; @@ -788,7 +788,7 @@ export class Aes256 implements BackupAlgorithm { public get untrusted(): boolean { return false; } - public encryptSession(data: Record): Promise { + public encryptSession(data: Record): Promise { const plainText: Record = Object.assign({}, data); delete plainText.session_id; delete plainText.room_id; diff --git a/src/crypto/dehydration.ts b/src/crypto/dehydration.ts index cc6b252da..1ed16b74c 100644 --- a/src/crypto/dehydration.ts +++ b/src/crypto/dehydration.ts @@ -258,7 +258,7 @@ export class DehydrationManager { } logger.log("Preparing fallback keys"); - const fallbackKeys = {}; + const fallbackKeys: Record = {}; for (const [keyId, key] of Object.entries(fallbacks.curve25519)) { const k: IOneTimeKey = { key, fallback: true }; const signature = account.sign(anotherjson.stringify(k)); diff --git a/src/crypto/deviceinfo.ts b/src/crypto/deviceinfo.ts index 3b4d53f68..a8fd9f008 100644 --- a/src/crypto/deviceinfo.ts +++ b/src/crypto/deviceinfo.ts @@ -72,7 +72,8 @@ export class DeviceInfo { const res = new DeviceInfo(deviceId); for (const prop in obj) { if (obj.hasOwnProperty(prop)) { - res[prop] = obj[prop]; + // @ts-ignore - this is messy and typescript doesn't like it + res[prop as keyof IDevice] = obj[prop as keyof IDevice]; } } return res; diff --git a/src/crypto/index.ts b/src/crypto/index.ts index d71fadf90..9163ace4f 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -88,6 +88,9 @@ import { CryptoStore } from "./store/base"; import { IVerificationChannel } from "./verification/request/Channel"; import { TypedEventEmitter } from "../models/typed-event-emitter"; import { IContent } from "../models/event"; +import { ISyncResponse } from "../sync-accumulator"; +import { ISignatures } from "../@types/signed"; +import { IMessage } from "./algorithms/olm"; const DeviceVerification = DeviceInfo.DeviceVerification; @@ -100,7 +103,7 @@ const defaultVerificationMethods = { // to start. [SHOW_QR_CODE_METHOD]: IllegalMethod, [SCAN_QR_CODE_METHOD]: IllegalMethod, -}; +} as const; /** * verification method names @@ -109,7 +112,7 @@ const defaultVerificationMethods = { export const verificationMethods = { RECIPROCATE_QR_CODE: ReciprocateQRCode.NAME, SAS: SASVerification.NAME, -}; +} as const; export type VerificationMethod = keyof typeof verificationMethods | string; @@ -200,18 +203,13 @@ interface IUserOlmSession { }[]; } -interface ISyncDeviceLists { - changed: string[]; - left: string[]; -} - export interface IRoomKeyRequestRecipient { userId: string; deviceId: string; } interface ISignableObject { - signatures?: object; + signatures?: ISignatures; unsigned?: object; } @@ -231,14 +229,25 @@ export interface IRequestsMap { } /* eslint-disable camelcase */ -export interface IEncryptedContent { - algorithm: string; +export interface IOlmEncryptedContent { + algorithm: typeof olmlib.OLM_ALGORITHM; sender_key: string; - ciphertext: Record; - [ToDeviceMessageId]: string; + ciphertext: Record; + [ToDeviceMessageId]?: string; +} + +export interface IMegolmEncryptedContent { + algorithm: typeof olmlib.MEGOLM_ALGORITHM; + sender_key: string; + session_id: string; + device_id: string; + ciphertext: string; + [ToDeviceMessageId]?: string; } /* eslint-enable camelcase */ +export type IEncryptedContent = IOlmEncryptedContent | IMegolmEncryptedContent; + export enum CryptoEvent { DeviceVerificationChanged = "deviceVerificationChanged", UserTrustStatusChanged = "userTrustStatusChanged", @@ -382,7 +391,7 @@ export class Crypto extends TypedEventEmitter, + verificationMethods: Array, ) { super(); this.reEmitter = new TypedReEmitter(this); @@ -2947,7 +2956,10 @@ export class Crypto extends TypedEventEmitter { + public async handleDeviceListChanges( + syncData: ISyncStateData, + syncDeviceLists: Required["device_lists"], + ): Promise { // Initial syncs don't have device change lists. We'll either get the complete list // of changes for the interval or will have invalidated everything in willProcessSync if (!syncData.oldSyncToken) return; @@ -3087,15 +3099,14 @@ export class Crypto extends TypedEventEmitter { - if (deviceLists.changed && Array.isArray(deviceLists.changed)) { + private async evalDeviceListChanges(deviceLists: Required["device_lists"]): Promise { + if (Array.isArray(deviceLists?.changed)) { deviceLists.changed.forEach((u) => { this.deviceList.invalidateUserDeviceList(u); }); } - if (deviceLists.left && Array.isArray(deviceLists.left) && - deviceLists.left.length) { + if (Array.isArray(deviceLists?.left) && deviceLists.left.length) { // Check we really don't share any rooms with these users // any more: the server isn't required to give us the // exact correct set. @@ -3515,9 +3526,9 @@ export class Crypto extends TypedEventEmitter { + public async signObject(obj: T): Promise { const sigs = obj.signatures || {}; const unsigned = obj.unsigned; diff --git a/src/crypto/olmlib.ts b/src/crypto/olmlib.ts index 111e4c16d..862691353 100644 --- a/src/crypto/olmlib.ts +++ b/src/crypto/olmlib.ts @@ -31,6 +31,7 @@ import { IClaimOTKsResult, MatrixClient } from "../client"; import { ISignatures } from "../@types/signed"; import { MatrixEvent } from "../models/event"; import { EventType } from "../@types/event"; +import { IMessage } from "./algorithms/olm"; enum Algorithm { Olm = "m.olm.v1.curve25519-aes-sha2", @@ -75,7 +76,7 @@ export interface IOlmSessionResult { * has been encrypted into `resultsObject` */ export async function encryptMessageForDevice( - resultsObject: Record, + resultsObject: Record, ourUserId: string, ourDeviceId: string | undefined, olmDevice: OlmDevice, @@ -124,6 +125,7 @@ export async function encryptMessageForDevice( recipient_keys: { "ed25519": recipientDevice.getFingerprint(), }, + ...payloadFields, }; // TODO: technically, a bunch of that stuff only needs to be included for @@ -131,11 +133,7 @@ export async function encryptMessageForDevice( // involved in the session. If we're looking to reduce data transfer in the // future, we could elide them for subsequent messages. - Object.assign(payload, payloadFields); - - resultsObject[deviceKey] = await olmDevice.encryptMessage( - deviceKey, sessionId, JSON.stringify(payload), - ); + resultsObject[deviceKey] = await olmDevice.encryptMessage(deviceKey, sessionId, JSON.stringify(payload)); } interface IExistingOlmSession { @@ -495,7 +493,7 @@ export async function verifySignature( * @param {string} pubKey The public key (ignored if key is a seed) * @returns {string} the signature for the object */ -export function pkSign(obj: IObject, key: PkSigning, userId: string, pubKey: string): string { +export function pkSign(obj: object & IObject, key: Uint8Array | PkSigning, userId: string, pubKey: string): string { let createdKey = false; if (key instanceof Uint8Array) { const keyObj = new global.Olm.PkSigning(); diff --git a/src/crypto/store/base.ts b/src/crypto/store/base.ts index 061582227..72b02e0a1 100644 --- a/src/crypto/store/base.ts +++ b/src/crypto/store/base.ts @@ -69,7 +69,7 @@ export interface CryptoStore { deleteOutgoingRoomKeyRequest(requestId: string, expectedState: number): Promise; // Olm Account - getAccount(txn: unknown, func: (accountPickle: string | null) => void); + getAccount(txn: unknown, func: (accountPickle: string | null) => void): void; storeAccount(txn: unknown, accountPickle: string): void; getCrossSigningKeys(txn: unknown, func: (keys: Record | null) => void): void; getSecretStorePrivateKey( diff --git a/src/crypto/store/localStorage-crypto-store.ts b/src/crypto/store/localStorage-crypto-store.ts index 977236ef9..c6f9a02d6 100644 --- a/src/crypto/store/localStorage-crypto-store.ts +++ b/src/crypto/store/localStorage-crypto-store.ts @@ -338,20 +338,20 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore { } public unmarkSessionsNeedingBackup(sessions: ISession[]): Promise { - const sessionsNeedingBackup - = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + const sessionsNeedingBackup = getJsonItem<{ + [senderKeySessionId: string]: string; + }>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; for (const session of sessions) { delete sessionsNeedingBackup[session.senderKey + '/' + session.sessionId]; } - setJsonItem( - this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup, - ); + setJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP, sessionsNeedingBackup); return Promise.resolve(); } public markSessionsNeedingBackup(sessions: ISession[]): Promise { - const sessionsNeedingBackup - = getJsonItem(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; + const sessionsNeedingBackup = getJsonItem<{ + [senderKeySessionId: string]: boolean; + }>(this.store, KEY_SESSIONS_NEEDING_BACKUP) || {}; for (const session of sessions) { sessionsNeedingBackup[session.senderKey + '/' + session.sessionId] = true; } diff --git a/src/crypto/verification/QRCode.ts b/src/crypto/verification/QRCode.ts index f6bdda17e..13053280d 100644 --- a/src/crypto/verification/QRCode.ts +++ b/src/crypto/verification/QRCode.ts @@ -283,21 +283,21 @@ export class QRCodeData { private static generateBuffer(qrData: IQrData): Buffer { let buf = Buffer.alloc(0); // we'll concat our way through life - const appendByte = (b): void => { + const appendByte = (b: number): void => { const tmpBuf = Buffer.from([b]); buf = Buffer.concat([buf, tmpBuf]); }; - const appendInt = (i): void => { + const appendInt = (i: number): void => { const tmpBuf = Buffer.alloc(2); tmpBuf.writeInt16BE(i, 0); buf = Buffer.concat([buf, tmpBuf]); }; - const appendStr = (s, enc, withLengthPrefix = true): void => { + const appendStr = (s: string, enc: BufferEncoding, withLengthPrefix = true): void => { const tmpBuf = Buffer.from(s, enc); if (withLengthPrefix) appendInt(tmpBuf.byteLength); buf = Buffer.concat([buf, tmpBuf]); }; - const appendEncBase64 = (b64): void => { + const appendEncBase64 = (b64: string): void => { const b = decodeBase64(b64); const tmpBuf = Buffer.from(b); buf = Buffer.concat([buf, tmpBuf]); @@ -307,7 +307,7 @@ export class QRCodeData { appendStr(qrData.prefix, "ascii", false); appendByte(qrData.version); appendByte(qrData.mode); - appendStr(qrData.transactionId, "utf-8"); + appendStr(qrData.transactionId!, "utf-8"); appendEncBase64(qrData.firstKeyB64); appendEncBase64(qrData.secondKeyB64); appendEncBase64(qrData.secretB64); diff --git a/src/crypto/verification/SAS.ts b/src/crypto/verification/SAS.ts index 5df6d48f6..168a85d28 100644 --- a/src/crypto/verification/SAS.ts +++ b/src/crypto/verification/SAS.ts @@ -140,7 +140,7 @@ function generateEmojiSas(sasBytes: number[]): EmojiMapping[] { const sasGenerators = { decimal: generateDecimalSas, emoji: generateEmojiSas, -}; +} as const; export interface IGeneratedSas { decimal?: [number, number, number]; @@ -154,11 +154,12 @@ export interface ISasEvent { mismatch(): void; } -function generateSas(sasBytes: number[], methods: string[]): IGeneratedSas { +function generateSas(sasBytes: Uint8Array, methods: string[]): IGeneratedSas { const sas: IGeneratedSas = {}; for (const method of methods) { if (method in sasGenerators) { - sas[method] = sasGenerators[method](sasBytes); + // @ts-ignore - ts doesn't like us mixing types like this + sas[method] = sasGenerators[method](Array.from(sasBytes)); } } return sas; @@ -168,15 +169,14 @@ const macMethods = { "hkdf-hmac-sha256": "calculate_mac", "org.matrix.msc3783.hkdf-hmac-sha256": "calculate_mac_fixed_base64", "hmac-sha256": "calculate_mac_long_kdf", -}; +} as const; -type Method = keyof typeof macMethods; +type MacMethod = keyof typeof macMethods; -function calculateMAC(olmSAS: OlmSAS, method: Method) { - return function(...args): string { - const macFunction = olmSAS[macMethods[method]]; - const mac: string = macFunction.apply(olmSAS, args); - logger.log("SAS calculateMAC:", method, args, mac); +function calculateMAC(olmSAS: OlmSAS, method: MacMethod) { + return function(input: string, info: string): string { + const mac = olmSAS[macMethods[method]](input, info); + logger.log("SAS calculateMAC:", method, [input, info], mac); return mac; }; } @@ -202,15 +202,17 @@ const calculateKeyAgreement = { + sas.channel.transactionId; return olmSAS.generate_bytes(sasInfo, bytes); }, -}; +} as const; + +type KeyAgreement = keyof typeof calculateKeyAgreement; /* lists of algorithms/methods that are supported. The key agreement, hashes, * and MAC lists should be sorted in order of preference (most preferred * first). */ -const KEY_AGREEMENT_LIST = ["curve25519-hkdf-sha256", "curve25519"]; +const KEY_AGREEMENT_LIST: KeyAgreement[] = ["curve25519-hkdf-sha256", "curve25519"]; const HASHES_LIST = ["sha256"]; -const MAC_LIST: Method[] = ["org.matrix.msc3783.hkdf-hmac-sha256", "hkdf-hmac-sha256", "hmac-sha256"]; +const MAC_LIST: MacMethod[] = ["org.matrix.msc3783.hkdf-hmac-sha256", "hkdf-hmac-sha256", "hmac-sha256"]; const SAS_LIST = Object.keys(sasGenerators); const KEY_AGREEMENT_SET = new Set(KEY_AGREEMENT_LIST); @@ -299,10 +301,10 @@ export class SAS extends Base { } private async verifyAndCheckMAC( - keyAgreement: string, + keyAgreement: KeyAgreement, sasMethods: string[], olmSAS: OlmSAS, - macMethod: Method, + macMethod: MacMethod, ): Promise { const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6); const verifySAS = new Promise((resolve, reject) => { @@ -354,7 +356,7 @@ export class SAS extends Base { throw new SwitchStartEventError(this.startEvent); } - let e; + let e: MatrixEvent; try { e = await this.waitForEvent(EventType.KeyVerificationAccept); } finally { @@ -445,8 +447,8 @@ export class SAS extends Base { } } - private sendMAC(olmSAS: OlmSAS, method: Method): Promise { - const mac = {}; + private sendMAC(olmSAS: OlmSAS, method: MacMethod): Promise { + const mac: Record = {}; const keyList: string[] = []; const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + this.baseApis.getUserId() + this.baseApis.deviceId @@ -455,7 +457,7 @@ export class SAS extends Base { const deviceKeyId = `ed25519:${this.baseApis.deviceId}`; mac[deviceKeyId] = calculateMAC(olmSAS, method)( - this.baseApis.getDeviceEd25519Key(), + this.baseApis.getDeviceEd25519Key()!, baseInfo + deviceKeyId, ); keyList.push(deviceKeyId); @@ -477,7 +479,7 @@ export class SAS extends Base { return this.send(EventType.KeyVerificationMac, { mac, keys }); } - private async checkMAC(olmSAS: OlmSAS, content: IContent, method: Method): Promise { + private async checkMAC(olmSAS: OlmSAS, content: IContent, method: MacMethod): Promise { const baseInfo = "MATRIX_KEY_VERIFICATION_MAC" + this.userId + this.deviceId + this.baseApis.getUserId() + this.baseApis.deviceId diff --git a/src/filter-component.ts b/src/filter-component.ts index 85afb0ea7..0bda7d379 100644 --- a/src/filter-component.ts +++ b/src/filter-component.ts @@ -148,17 +148,17 @@ export class FilterComponent { "types": function(v: string): boolean { return matchesWildcard(eventType, v); }, - }; + } as const; for (const name in literalKeys) { - const matchFunc = literalKeys[name]; + const matchFunc = literalKeys[name]; const notName = "not_" + name; - const disallowedValues: string[] = this.filterJson[notName]; + const disallowedValues = this.filterJson[<`not_${keyof typeof literalKeys}`>notName]; if (disallowedValues?.some(matchFunc)) { return false; } - const allowedValues: string[] = this.filterJson[name]; + const allowedValues = this.filterJson[name as keyof typeof literalKeys]; if (allowedValues && !allowedValues.some(matchFunc)) { return false; } diff --git a/src/filter.ts b/src/filter.ts index 57bd0540d..665227017 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -31,8 +31,8 @@ import { MatrixEvent } from "./models/event"; * @param {string} keyNesting * @param {*} val */ -function setProp(obj: object, keyNesting: string, val: any): void { - const nestedKeys = keyNesting.split("."); +function setProp(obj: Record, keyNesting: string, val: any): void { + const nestedKeys = keyNesting.split(".") as [keyof typeof obj]; let currentObj = obj; for (let i = 0; i < (nestedKeys.length - 1); i++) { if (!currentObj[nestedKeys[i]]) { @@ -70,8 +70,7 @@ interface IStateFilter extends IRoomEventFilter {} interface IRoomFilter { not_rooms?: string[]; - rooms?: string[]; - ephemeral?: IRoomEventFilter; + rooms?: string[];ephemeral?: IRoomEventFilter; include_leave?: boolean; state?: IStateFilter; timeline?: IRoomEventFilter; diff --git a/src/models/event.ts b/src/models/event.ts index c233307d4..2864aa6b5 100644 --- a/src/models/event.ts +++ b/src/models/event.ts @@ -318,19 +318,19 @@ export class MatrixEvent extends TypedEventEmitter { + (["state_key", "type", "sender", "room_id", "membership"] as const).forEach((prop) => { if (typeof event[prop] !== "string") return; - event[prop] = internaliseString(event[prop]); + event[prop] = internaliseString(event[prop]!); }); - ["membership", "avatar_url", "displayname"].forEach((prop) => { + (["membership", "avatar_url", "displayname"] as const).forEach((prop) => { if (typeof event.content?.[prop] !== "string") return; - event.content[prop] = internaliseString(event.content[prop]); + event.content[prop] = internaliseString(event.content[prop]!); }); - ["rel_type"].forEach((prop) => { + (["rel_type"] as const).forEach((prop) => { if (typeof event.content?.["m.relates_to"]?.[prop] !== "string") return; - event.content["m.relates_to"][prop] = internaliseString(event.content["m.relates_to"][prop]); + event.content["m.relates_to"][prop] = internaliseString(event.content["m.relates_to"][prop]!); }); this.txnId = event.txn_id; @@ -1120,7 +1120,7 @@ export class MatrixEvent extends TypedEventEmitter> = { [EventType.RoomMember]: { 'membership': 1 }, [EventType.RoomCreate]: { 'creator': 1 }, [EventType.RoomJoinRules]: { 'join_rule': 1 }, @@ -1621,7 +1624,7 @@ const REDACT_KEEP_CONTENT_MAP = { 'kick': 1, 'redact': 1, 'state_default': 1, 'users': 1, 'users_default': 1, }, -}; +} as const; /** * Fires when an event is decrypted diff --git a/src/models/invites-ignorer.ts b/src/models/invites-ignorer.ts index ad48fe563..907d1ca17 100644 --- a/src/models/invites-ignorer.ts +++ b/src/models/invites-ignorer.ts @@ -17,7 +17,7 @@ limitations under the License. import { UnstableValue } from "matrix-events-sdk"; import { MatrixClient } from "../client"; -import { MatrixEvent } from "./event"; +import { IContent, MatrixEvent } from "./event"; import { EventTimeline } from "./event-timeline"; import { Preset } from "../@types/partials"; import { globToRegexp } from "../utils"; @@ -262,7 +262,7 @@ export class IgnoredInvites { */ public async getOrCreateSourceRooms(): Promise { const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies(); - let sources = ignoreInvitesPolicies.sources; + let sources: string[] = ignoreInvitesPolicies.sources; // Validate `sources`. If it is invalid, trash out the current `sources` // and create a new list of sources from `target`. @@ -272,11 +272,11 @@ export class IgnoredInvites { hasChanges = true; sources = []; } - let sourceRooms: Room[] = sources + let sourceRooms = sources // `sources` could contain non-string / invalid room ids .filter(roomId => typeof roomId === "string") .map(roomId => this.client.getRoom(roomId)) - .filter(room => !!room); + .filter(room => !!room) as Room[]; if (sourceRooms.length != sources.length) { hasChanges = true; } @@ -327,7 +327,7 @@ export class IgnoredInvites { */ private getPoliciesAndIgnoreInvitesPolicies(): {policies: {[key: string]: any}, ignoreInvitesPolicies: {[key: string]: any}} { - let policies = {}; + let policies: IContent = {}; for (const key of [POLICIES_ACCOUNT_EVENT_TYPE.name, POLICIES_ACCOUNT_EVENT_TYPE.altName]) { if (!key) { continue; diff --git a/src/models/room-state.ts b/src/models/room-state.ts index 523201d83..e69d95152 100644 --- a/src/models/room-state.ts +++ b/src/models/room-state.ts @@ -22,7 +22,7 @@ import { RoomMember } from "./room-member"; import { logger } from '../logger'; import * as utils from "../utils"; import { EventType, UNSTABLE_MSC2716_MARKER } from "../@types/event"; -import { MatrixEvent, MatrixEventEvent } from "./event"; +import { IEvent, MatrixEvent, MatrixEventEvent } from "./event"; import { MatrixClient } from "../client"; import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials"; import { TypedEventEmitter } from "./typed-event-emitter"; @@ -53,6 +53,20 @@ enum OobStatus { Finished, } +export interface IPowerLevelsContent { + users?: Record; + events?: Record; + // eslint-disable-next-line camelcase + users_default?: number; + // eslint-disable-next-line camelcase + events_default?: number; + // eslint-disable-next-line camelcase + state_default?: number; + ban?: number; + kick?: number; + redact?: number; +} + export enum RoomStateEvent { Events = "RoomState.events", Members = "RoomState.members", @@ -499,7 +513,7 @@ export class RoomState extends TypedEventEmitter const beacon = this.beacons.get(beaconIdentifier)!; if (event.isRedacted()) { - if (beacon.beaconInfoId === event.getRedactionEvent()?.['redacts']) { + if (beacon.beaconInfoId === (event.getRedactionEvent())?.redacts) { beacon.destroy(); this.beacons.delete(beaconIdentifier); } @@ -724,17 +738,17 @@ export class RoomState extends TypedEventEmitter * @param {number} powerLevel The power level of the member * @return {boolean} true if the given power level is sufficient */ - public hasSufficientPowerLevelFor(action: string, powerLevel: number): boolean { + public hasSufficientPowerLevelFor(action: "ban" | "kick" | "redact", powerLevel: number): boolean { const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, ""); - let powerLevels = {}; + let powerLevels: IPowerLevelsContent = {}; if (powerLevelsEvent) { powerLevels = powerLevelsEvent.getContent(); } let requiredLevel = 50; if (utils.isNumber(powerLevels[action])) { - requiredLevel = powerLevels[action]; + requiredLevel = powerLevels[action]!; } return powerLevel >= requiredLevel; @@ -807,8 +821,8 @@ export class RoomState extends TypedEventEmitter private maySendEventOfType(eventType: EventType | string, userId: string, state: boolean): boolean { const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, ''); - let powerLevels; - let eventsLevels = {}; + let powerLevels: IPowerLevelsContent; + let eventsLevels: Record = {}; let stateDefault = 0; let eventsDefault = 0; @@ -818,20 +832,20 @@ export class RoomState extends TypedEventEmitter eventsLevels = powerLevels.events || {}; if (Number.isSafeInteger(powerLevels.state_default)) { - stateDefault = powerLevels.state_default; + stateDefault = powerLevels.state_default!; } else { stateDefault = 50; } const userPowerLevel = powerLevels.users && powerLevels.users[userId]; if (Number.isSafeInteger(userPowerLevel)) { - powerLevel = userPowerLevel; + powerLevel = userPowerLevel!; } else if (Number.isSafeInteger(powerLevels.users_default)) { - powerLevel = powerLevels.users_default; + powerLevel = powerLevels.users_default!; } if (Number.isSafeInteger(powerLevels.events_default)) { - eventsDefault = powerLevels.events_default; + eventsDefault = powerLevels.events_default!; } } @@ -877,7 +891,7 @@ export class RoomState extends TypedEventEmitter */ public getJoinRule(): JoinRule { const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, ""); - const joinRuleContent = joinRuleEvent?.getContent() ?? {}; + const joinRuleContent: Partial = joinRuleEvent?.getContent() ?? {}; return joinRuleContent["join_rule"] || JoinRule.Invite; } diff --git a/src/models/room-summary.ts b/src/models/room-summary.ts index ba279d2d4..218a4afcc 100644 --- a/src/models/room-summary.ts +++ b/src/models/room-summary.ts @@ -20,8 +20,8 @@ limitations under the License. export interface IRoomSummary { "m.heroes": string[]; - "m.joined_member_count": number; - "m.invited_member_count": number; + "m.joined_member_count"?: number; + "m.invited_member_count"?: number; } interface IInfo { diff --git a/src/models/room.ts b/src/models/room.ts index 3952ed7d9..4c3ebafbe 100644 --- a/src/models/room.ts +++ b/src/models/room.ts @@ -1312,10 +1312,10 @@ export class Room extends ReadReceipt { const joinedCount = summary["m.joined_member_count"]; const invitedCount = summary["m.invited_member_count"]; if (Number.isInteger(joinedCount)) { - this.currentState.setJoinedMemberCount(joinedCount); + this.currentState.setJoinedMemberCount(joinedCount!); } if (Number.isInteger(invitedCount)) { - this.currentState.setInvitedMemberCount(invitedCount); + this.currentState.setInvitedMemberCount(invitedCount!); } if (Array.isArray(heroes)) { // be cautious about trusting server values, @@ -1826,7 +1826,7 @@ export class Room extends ReadReceipt { timelineSet.getFilter(), ); - timelineSet.getLiveTimeline().setPaginationToken(end, Direction.Backward); + timelineSet.getLiveTimeline().setPaginationToken(end ?? null, Direction.Backward); if (!events.length) return; diff --git a/src/pushprocessor.ts b/src/pushprocessor.ts index 4538b9721..cc1246825 100644 --- a/src/pushprocessor.ts +++ b/src/pushprocessor.ts @@ -518,13 +518,13 @@ export class PushProcessor { * @return {object} The push rule, or null if no such rule was found */ public getPushRuleById(ruleId: string): IPushRule | null { - for (const scope of ['global']) { + for (const scope of ['global'] as const) { if (this.client.pushRules?.[scope] === undefined) continue; for (const kind of RULEKINDS_IN_ORDER) { if (this.client.pushRules[scope][kind] === undefined) continue; - for (const rule of this.client.pushRules[scope][kind]) { + for (const rule of this.client.pushRules[scope][kind]!) { if (rule.rule_id === ruleId) return rule; } } diff --git a/src/scheduler.ts b/src/scheduler.ts index 0c15032bc..74bf7dd25 100644 --- a/src/scheduler.ts +++ b/src/scheduler.ts @@ -97,7 +97,7 @@ export class MatrixScheduler { * @see module:scheduler~queueAlgorithm */ // eslint-disable-next-line @typescript-eslint/naming-convention - public static QUEUE_MESSAGES(event: MatrixEvent): "message" | null { + public static QUEUE_MESSAGES(event: MatrixEvent): string | null { // enqueue messages or events that associate with another event (redactions and relations) if (event.getType() === EventType.RoomMessage || event.hasAssociation()) { // put these events in the 'message' queue. diff --git a/src/sliding-sync-sdk.ts b/src/sliding-sync-sdk.ts index c99f61f13..d103eb39b 100644 --- a/src/sliding-sync-sdk.ts +++ b/src/sliding-sync-sdk.ts @@ -22,7 +22,7 @@ import { ClientEvent, IStoredClientOpts, MatrixClient, PendingEventOrdering } fr import { ISyncStateData, SyncState, _createAndReEmitRoom } from "./sync"; import { MatrixEvent } from "./models/event"; import { Crypto } from "./crypto"; -import { IMinimalEvent, IRoomEvent, IStateEvent, IStrippedState } from "./sync-accumulator"; +import { IMinimalEvent, IRoomEvent, IStateEvent, IStrippedState, ISyncResponse } from "./sync-accumulator"; import { MatrixError } from "./http-api"; import { Extension, @@ -44,7 +44,18 @@ import { RoomMemberEvent } from "./models/room-member"; // keepAlive is successful but the server /sync fails. const FAILED_SYNC_ERROR_THRESHOLD = 3; -class ExtensionE2EE implements Extension { +type ExtensionE2EERequest = { + enabled: boolean; +}; + +type ExtensionE2EEResponse = Pick; + +class ExtensionE2EE implements Extension { public constructor(private readonly crypto: Crypto) {} public name(): string { @@ -55,7 +66,7 @@ class ExtensionE2EE implements Extension { return ExtensionState.PreProcess; } - public onRequest(isInitial: boolean): object | undefined { + public onRequest(isInitial: boolean): ExtensionE2EERequest | undefined { if (!isInitial) { return undefined; } @@ -64,7 +75,7 @@ class ExtensionE2EE implements Extension { }; } - public async onResponse(data: object): Promise { + public async onResponse(data: ExtensionE2EEResponse): Promise { // Handle device list updates if (data["device_lists"]) { await this.crypto.handleDeviceListChanges({ @@ -92,7 +103,18 @@ class ExtensionE2EE implements Extension { } } -class ExtensionToDevice implements Extension { +type ExtensionToDeviceRequest = { + since?: string; + limit?: number; + enabled?: boolean; +}; + +type ExtensionToDeviceResponse = { + events: Required["to_device"]["events"]; + next_batch: string | null; +}; + +class ExtensionToDevice implements Extension { private nextBatch: string | null = null; public constructor(private readonly client: MatrixClient) {} @@ -105,8 +127,8 @@ class ExtensionToDevice implements Extension { return ExtensionState.PreProcess; } - public onRequest(isInitial: boolean): object { - const extReq = { + public onRequest(isInitial: boolean): ExtensionToDeviceRequest { + const extReq: ExtensionToDeviceRequest = { since: this.nextBatch !== null ? this.nextBatch : undefined, }; if (isInitial) { @@ -116,11 +138,10 @@ class ExtensionToDevice implements Extension { return extReq; } - public async onResponse(data: object): Promise { + public async onResponse(data: ExtensionToDeviceResponse): Promise { const cancelledKeyVerificationTxns: string[] = []; - data["events"] = data["events"] || []; - data["events"] - .map(this.client.getEventMapper()) + data.events + ?.map(this.client.getEventMapper()) .map((toDeviceEvent) => { // map is a cheap inline forEach // We want to flag m.key.verification.start events as cancelled // if there's an accompanying m.key.verification.cancel event, so @@ -165,11 +186,20 @@ class ExtensionToDevice implements Extension { }, ); - this.nextBatch = data["next_batch"]; + this.nextBatch = data.next_batch; } } -class ExtensionAccountData implements Extension { +type ExtensionAccountDataRequest = { + enabled: boolean; +}; + +type ExtensionAccountDataResponse = { + global: IMinimalEvent[]; + rooms: Record; +}; + +class ExtensionAccountData implements Extension { public constructor(private readonly client: MatrixClient) {} public name(): string { @@ -180,7 +210,7 @@ class ExtensionAccountData implements Extension { return ExtensionState.PostProcess; } - public onRequest(isInitial: boolean): object | undefined { + public onRequest(isInitial: boolean): ExtensionAccountDataRequest | undefined { if (!isInitial) { return undefined; } @@ -189,7 +219,7 @@ class ExtensionAccountData implements Extension { }; } - public onResponse(data: {global: object[], rooms: Record}): void { + public onResponse(data: ExtensionAccountDataResponse): void { if (data.global && data.global.length > 0) { this.processGlobalAccountData(data.global); } @@ -208,9 +238,9 @@ class ExtensionAccountData implements Extension { } } - private processGlobalAccountData(globalAccountData: object[]): void { + private processGlobalAccountData(globalAccountData: IMinimalEvent[]): void { const events = mapEvents(this.client, undefined, globalAccountData); - const prevEventsMap = events.reduce((m, c) => { + const prevEventsMap = events.reduce>((m, c) => { m[c.getType()] = this.client.store.getAccountData(c.getType()); return m; }, {}); @@ -233,7 +263,15 @@ class ExtensionAccountData implements Extension { } } -class ExtensionTyping implements Extension { +type ExtensionTypingRequest = { + enabled: boolean; +}; + +type ExtensionTypingResponse = { + rooms: Record; +}; + +class ExtensionTyping implements Extension { public constructor(private readonly client: MatrixClient) {} public name(): string { @@ -244,7 +282,7 @@ class ExtensionTyping implements Extension { return ExtensionState.PostProcess; } - public onRequest(isInitial: boolean): object | undefined { + public onRequest(isInitial: boolean): ExtensionTypingRequest | undefined { if (!isInitial) { return undefined; // don't send a JSON object for subsequent requests, we don't need to. } @@ -253,20 +291,26 @@ class ExtensionTyping implements Extension { }; } - public onResponse(data: {rooms: Record}): void { - if (!data || !data.rooms) { + public onResponse(data: ExtensionTypingResponse): void { + if (!data?.rooms) { return; } for (const roomId in data.rooms) { - processEphemeralEvents( - this.client, roomId, [data.rooms[roomId]], - ); + processEphemeralEvents(this.client, roomId, [data.rooms[roomId]]); } } } -class ExtensionReceipts implements Extension { +type ExtensionReceiptsRequest = { + enabled: boolean; +}; + +type ExtensionReceiptsResponse = { + rooms: Record; +}; + +class ExtensionReceipts implements Extension { public constructor(private readonly client: MatrixClient) {} public name(): string { @@ -277,7 +321,7 @@ class ExtensionReceipts implements Extension { return ExtensionState.PostProcess; } - public onRequest(isInitial: boolean): object | undefined { + public onRequest(isInitial: boolean): ExtensionReceiptsRequest | undefined { if (isInitial) { return { enabled: true, @@ -286,8 +330,8 @@ class ExtensionReceipts implements Extension { return undefined; // don't send a JSON object for subsequent requests, we don't need to. } - public onResponse(data: {rooms: Record}): void { - if (!data || !data.rooms) { + public onResponse(data: ExtensionReceiptsResponse): void { + if (!data?.rooms) { return; } @@ -336,7 +380,7 @@ export class SlidingSyncSdk { this.slidingSync.on(SlidingSyncEvent.Lifecycle, this.onLifecycle.bind(this)); this.slidingSync.on(SlidingSyncEvent.RoomData, this.onRoomData.bind(this)); - const extensions: Extension[] = [ + const extensions: Extension[] = [ new ExtensionToDevice(this.client), new ExtensionAccountData(this.client), new ExtensionTyping(this.client), @@ -533,7 +577,7 @@ export class SlidingSyncSdk { // room::decryptCriticalEvent is in charge of decrypting all the events // required for a client to function properly let timelineEvents = mapEvents(this.client, room.roomId, roomData.timeline, false); - const ephemeralEvents = []; // TODO this.mapSyncEventsFormat(joinObj.ephemeral); + const ephemeralEvents: MatrixEvent[] = []; // TODO this.mapSyncEventsFormat(joinObj.ephemeral); // TODO: handle threaded / beacon events @@ -967,12 +1011,14 @@ function ensureNameEvent(client: MatrixClient, roomId: string, roomData: MSC3575 return roomData; } +type TaggedEvent = (IStrippedState | IRoomEvent | IStateEvent | IMinimalEvent) & { room_id?: string }; + // 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 | undefined, events: object[], decrypt = true): MatrixEvent[] { const mapper = client.getEventMapper({ decrypt }); - return (events as Array).map(function(e) { - e["room_id"] = roomId; + return (events as TaggedEvent[]).map(function(e) { + e.room_id = roomId; return mapper(e); }); } diff --git a/src/sliding-sync.ts b/src/sliding-sync.ts index cda2737d9..a3daf3af6 100644 --- a/src/sliding-sync.ts +++ b/src/sliding-sync.ts @@ -139,7 +139,7 @@ export interface MSC3575SlidingSyncResponse { txn_id?: string; lists: ListResponse[]; rooms: Record; - extensions: object; + extensions: Record; } export enum SlidingSyncState { @@ -265,7 +265,7 @@ export enum ExtensionState { /** * An interface that must be satisfied to register extensions */ -export interface Extension { +export interface Extension { /** * The extension name to go under 'extensions' in the request body. * @returns The JSON key. @@ -277,12 +277,12 @@ 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 | undefined; + onRequest(isInitial: boolean): Req | undefined; /** * A function which is called when there is response JSON under this extension. * @param data The response JSON under the extension name. */ - onResponse(data: object); + onResponse(data: Res): void; /** * Controls when onResponse should be called. * @returns The state when it should be called. @@ -353,7 +353,7 @@ export class SlidingSync extends TypedEventEmitter & { txnId: string})[] = []; // map of extension name to req/resp handler - private extensions: Record = {}; + private extensions: Record> = {}; private desiredRoomSubscriptions = new Set(); // the *desired* room subscriptions private confirmedRoomSubscriptions = new Set(); @@ -519,22 +519,22 @@ export class SlidingSync extends TypedEventEmitter): void { if (this.extensions[ext.name()]) { throw new Error(`registerExtension: ${ext.name()} already exists as an extension`); } this.extensions[ext.name()] = ext; } - private getExtensionRequest(isInitial: boolean): object { - const ext = {}; + private getExtensionRequest(isInitial: boolean): Record { + const ext: Record = {}; Object.keys(this.extensions).forEach((extName) => { ext[extName] = this.extensions[extName].onRequest(isInitial); }); return ext; } - private onPreExtensionsResponse(ext: object): void { + private onPreExtensionsResponse(ext: Record): void { Object.keys(ext).forEach((extName) => { if (this.extensions[extName].when() == ExtensionState.PreProcess) { this.extensions[extName].onResponse(ext[extName]); @@ -542,7 +542,7 @@ export class SlidingSync extends TypedEventEmitter): void { Object.keys(ext).forEach((extName) => { if (this.extensions[extName].when() == ExtensionState.PostProcess) { this.extensions[extName].onResponse(ext[extName]); diff --git a/src/store/index.ts b/src/store/index.ts index bed7aa7a6..3edba9269 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -123,7 +123,7 @@ export interface IStore { * @param {string} token The token associated with these events. * @param {boolean} toStart True if these are paginated results. */ - storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean): void; + storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void; /** * Store a filter. diff --git a/src/store/indexeddb-local-backend.ts b/src/store/indexeddb-local-backend.ts index af4b5fc65..2b94af256 100644 --- a/src/store/indexeddb-local-backend.ts +++ b/src/store/indexeddb-local-backend.ts @@ -315,10 +315,10 @@ export class LocalIndexedDBStoreBackend implements IIndexedDBBackend { const minStateKeyProm = reqAsCursorPromise( roomIndex.openKeyCursor(roomRange, "next"), - ).then((cursor) => cursor && cursor.primaryKey[1]); + ).then((cursor) => (cursor?.primaryKey)[1]); const maxStateKeyProm = reqAsCursorPromise( roomIndex.openKeyCursor(roomRange, "prev"), - ).then((cursor) => cursor && cursor.primaryKey[1]); + ).then((cursor) => (cursor?.primaryKey)[1]); const [minStateKey, maxStateKey] = await Promise.all( [minStateKeyProm, maxStateKeyProm]); diff --git a/src/store/memory.ts b/src/store/memory.ts index f24ab2d97..1fcb6b0c8 100644 --- a/src/store/memory.ts +++ b/src/store/memory.ts @@ -217,7 +217,7 @@ export class MemoryStore implements IStore { * @param {string} token The token associated with these events. * @param {boolean} toStart True if these are paginated results. */ - public storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean): void { + public storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void { // no-op because they've already been added to the room instance. } diff --git a/src/store/stub.ts b/src/store/stub.ts index 746bc521f..742343938 100644 --- a/src/store/stub.ts +++ b/src/store/stub.ts @@ -139,7 +139,7 @@ export class StubStore implements IStore { * @param {string} token The token associated with these events. * @param {boolean} toStart True if these are paginated results. */ - public storeEvents(room: Room, events: MatrixEvent[], token: string, toStart: boolean): void {} + public storeEvents(room: Room, events: MatrixEvent[], token: string | null, toStart: boolean): void {} /** * Store a filter. diff --git a/src/sync-accumulator.ts b/src/sync-accumulator.ts index 19d5cf131..f383cb98a 100644 --- a/src/sync-accumulator.ts +++ b/src/sync-accumulator.ts @@ -116,7 +116,7 @@ interface IAccountData { events: IMinimalEvent[]; } -interface IToDeviceEvent { +export interface IToDeviceEvent { content: IContent; sender: string; type: string; @@ -127,8 +127,8 @@ interface IToDevice { } interface IDeviceLists { - changed: string[]; - left: string[]; + changed?: string[]; + left?: string[]; } export interface ISyncResponse { @@ -139,6 +139,9 @@ export interface ISyncResponse { to_device?: IToDevice; device_lists?: IDeviceLists; device_one_time_keys_count?: Record; + + device_unused_fallback_key_types?: string[]; + "org.matrix.msc2732.device_unused_fallback_key_types"?: string[]; } /* eslint-enable camelcase */ @@ -182,6 +185,12 @@ export interface ISyncData { roomsData: IRooms; } +type TaggedEvent = IRoomEvent & { _localTs?: number }; + +function isTaggedEvent(event: IRoomEvent): event is TaggedEvent { + return "_localTs" in event && event["_localTs"] !== undefined; +} + /** * The purpose of this class is to accumulate /sync responses such that a * complete "initial" JSON response can be returned which accurately represents @@ -473,35 +482,31 @@ export class SyncAccumulator { // - existing state which didn't come down /sync. // - State events under the 'state' key. // - State events in the 'timeline'. - if (data.state && data.state.events) { - data.state.events.forEach((e) => { - setState(currentData._currentState, e); - }); - } - if (data.timeline && data.timeline.events) { - data.timeline.events.forEach((e, index) => { - // this nops if 'e' isn't a state event - setState(currentData._currentState, e); - // append the event to the timeline. The back-pagination token - // corresponds to the first event in the timeline - let transformedEvent: IRoomEvent & { _localTs?: number }; - if (!fromDatabase) { - transformedEvent = Object.assign({}, e); - if (transformedEvent.unsigned !== undefined) { - transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned); - } - const age = e.unsigned ? e.unsigned.age : e.age; - if (age !== undefined) transformedEvent._localTs = Date.now() - age; - } else { - transformedEvent = e; + data.state?.events?.forEach((e) => { + setState(currentData._currentState, e); + }); + data.timeline?.events?.forEach((e, index) => { + // this nops if 'e' isn't a state event + setState(currentData._currentState, e); + // append the event to the timeline. The back-pagination token + // corresponds to the first event in the timeline + let transformedEvent: TaggedEvent; + if (!fromDatabase) { + transformedEvent = Object.assign({}, e); + if (transformedEvent.unsigned !== undefined) { + transformedEvent.unsigned = Object.assign({}, transformedEvent.unsigned); } + const age = e.unsigned ? e.unsigned.age : e.age; + if (age !== undefined) transformedEvent._localTs = Date.now() - age; + } else { + transformedEvent = e; + } - currentData._timeline.push({ - event: transformedEvent, - token: index === 0 ? (data.timeline.prev_batch ?? null) : null, - }); + currentData._timeline.push({ + event: transformedEvent, + token: index === 0 ? (data.timeline.prev_batch ?? null) : null, }); - } + }); // attempt to prune the timeline by jumping between events which have // pagination tokens. @@ -581,7 +586,7 @@ export class SyncAccumulator { room_id: roomId, content: { // $event_id: { "m.read": { $user_id: $json } } - }, + } as IContent, }; for (const [userId, receiptData] of Object.entries(roomData._readReceipts)) { @@ -626,8 +631,8 @@ export class SyncAccumulator { } let transformedEvent: (IRoomEvent | IStateEvent) & { _localTs?: number }; - if (!forDatabase && msgData.event["_localTs"]) { - // This means we have to copy each event so we can fix it up to + if (!forDatabase && isTaggedEvent(msgData.event)) { + // This means we have to copy each event, so we can fix it up to // set a correct 'age' parameter whilst keeping the local timestamp // on our stored event. If this turns out to be a bottleneck, it could // be optimised either by doing this in the main process after the data @@ -641,7 +646,7 @@ export class SyncAccumulator { } delete transformedEvent._localTs; transformedEvent.unsigned = transformedEvent.unsigned || {}; - transformedEvent.unsigned.age = Date.now() - msgData.event["_localTs"]; + transformedEvent.unsigned.age = Date.now() - msgData.event._localTs!; } else { transformedEvent = msgData.event; } diff --git a/src/sync.ts b/src/sync.ts index 20cab67b6..c97ba3540 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -106,7 +106,7 @@ function getFilterName(userId: string, suffix?: string): string { return `FILTER_SYNC_${userId}` + (suffix ? "_" + suffix : ""); } -function debuglog(...params): void { +function debuglog(...params: any[]): void { if (!DEBUG) return; logger.log(...params); } @@ -1093,7 +1093,7 @@ export class SyncApi { // handle non-room account_data if (Array.isArray(data.account_data?.events)) { const events = data.account_data.events.map(client.getEventMapper()); - const prevEventsMap = events.reduce((m, c) => { + const prevEventsMap = events.reduce>((m, c) => { m[c.getType()!] = client.store.getAccountData(c.getType()); return m; }, {}); @@ -1473,12 +1473,13 @@ export class SyncApi { this.opts.crypto.updateOneTimeKeyCount(currentCount); } if (this.opts.crypto && - (data["device_unused_fallback_key_types"] || - data["org.matrix.msc2732.device_unused_fallback_key_types"])) { + (data.device_unused_fallback_key_types || + data["org.matrix.msc2732.device_unused_fallback_key_types"]) + ) { // The presence of device_unused_fallback_key_types indicates that the // server supports fallback keys. If there's no unused // signed_curve25519 fallback key we need a new one. - const unusedFallbackKeys = data["device_unused_fallback_key_types"] || + const unusedFallbackKeys = data.device_unused_fallback_key_types || data["org.matrix.msc2732.device_unused_fallback_key_types"]; this.opts.crypto.setNeedsNewFallback( Array.isArray(unusedFallbackKeys) && @@ -1607,9 +1608,10 @@ export class SyncApi { return []; } const mapper = this.client.getEventMapper({ decrypt }); - return (obj.events as Array).map(function(e) { + type TaggedEvent = (IStrippedState | IRoomEvent | IStateEvent | IMinimalEvent) & { room_id?: string }; + return (obj.events as TaggedEvent[]).map(function(e) { if (room) { - e["room_id"] = room.roomId; + e.room_id = room.roomId; } return mapper(e); }); diff --git a/src/utils.ts b/src/utils.ts index 71871d3b7..ef6af6b06 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -316,7 +316,7 @@ export function deepSortedObjectEntries(obj: any): [string, any][] { * @param {*} value the value to test * @return {boolean} whether or not value is a finite number without type-coercion */ -export function isNumber(value: any): boolean { +export function isNumber(value: any): value is number { return typeof value === 'number' && isFinite(value); } @@ -428,8 +428,8 @@ export interface IDeferred { // Returns a Deferred export function defer(): IDeferred { - let resolve; - let reject; + let resolve!: IDeferred["resolve"]; + let reject!: IDeferred["reject"]; const promise = new Promise((_resolve, _reject) => { resolve = _resolve; @@ -665,18 +665,22 @@ export function compare(a: string, b: string): number { * @param {Object} source * @returns the target object */ -export function recursivelyAssign(target: Object, source: Object, ignoreNullish = false): any { +export function recursivelyAssign>( + target: T1, + source: T2, + ignoreNullish = false, +): T1 & T2 { for (const [sourceKey, sourceValue] of Object.entries(source)) { if (target[sourceKey] instanceof Object && sourceValue) { recursivelyAssign(target[sourceKey], sourceValue); continue; } if ((sourceValue !== null && sourceValue !== undefined) || !ignoreNullish) { - target[sourceKey] = sourceValue; + target[sourceKey as keyof T1] = sourceValue; continue; } } - return target; + return target as T1 & T2; } function getContentTimestampWithFallback(event: MatrixEvent): number { diff --git a/src/webrtc/call.ts b/src/webrtc/call.ts index 178f75608..7d303af4a 100644 --- a/src/webrtc/call.ts +++ b/src/webrtc/call.ts @@ -27,7 +27,7 @@ import { parse as parseSdp, write as writeSdp } from "sdp-transform"; import { logger } from '../logger'; import * as utils from '../utils'; -import { MatrixEvent } from '../models/event'; +import { IContent, MatrixEvent } from '../models/event'; import { EventType, ToDeviceMessageId } from '../@types/event'; import { RoomMember } from '../models/room-member'; import { randomString } from '../randomstring'; @@ -1101,7 +1101,7 @@ export class MatrixCall extends TypedEventEmitter